Java Back-End Wizardry

FlexiCore documentation

2.Flexicore Documentation

General Class Diagram Notes

class diagrams in this documentation are not showing the full inheritance of the entities and include only the entities that reside in the same software component .

most entities in the class diagram have  Baseclass as direct or in-direct ancestor.

Acknowledgement

this documentation structure was inspired by Spring Boot Documentation.

3.Getting Started

If you are getting started with FlexiCore, start by reading this section. It answers the basic “what?”, “how?” and “why?” questions. It includes an introduction to FlexiCore, along with installation instructions. We then walk you through building your first FlexiCore application, discussing some core principles as we go.

3.1.Introducing FlexiCore

FlexiCore makes it easy to create plugable, production-grade Spring-based Applications that you can Run.

Everything is a Plugin

with the concept of service plugins and entities plugins it supports all of spring features in plugins ( dependency injection, rest APIs, event bus etc), this allows developers to create a logically separated units (with dependency among them) which are self contained, this allows for better source control ,faster development cycles and less bugs in production.

3.2.System Requirements

FlexiCore requires java 8 and is compatible up to java 14 (included).

FlexiCore requires a postgresql database  version 9+, a mongodb database version 3+ and maven 3.3+ to build.

3.3.Installing FlexiCore

FlexiCore can be used with “classic” Java development tools or installed as a command line tool. Either way, you need Java SDK v1.8 or higher. Before you begin, you should check your current Java installation by using the following command:

$ java -version

 

3.4.Developing Your First FlexiCore Application

This section describes how to develop a simple “Hello World!” web application that highlights some of FlexiCore’s key features. We use Maven to build this project since most IDEs support it.

Before we begin, open a terminal and run the following commands to ensure that you have valid versions of Java and Maven installed:

$ java -version
java version "1.8.0_102"
Java(TM) SE Runtime Environment (build 1.8.0_102-b14)
Java HotSpot(TM) 64-Bit Server VM (build 25.102-b14, mixed mode)
$ mvn -v
Apache Maven 3.5.4 (1edded0938998edf8bf061f1ceb3cfdeccf443fe; 2018-06-17T14:33:14-04:00)
Maven home: /usr/local/Cellar/maven/3.3.9/libexec
Java version: 1.8.0_102, vendor: Oracle Corporation

This sample needs to be created in its own directory. Subsequent instructions assume that you have created a suitable directory and that it is your current directory.

3.4.1.Creating the POM

We need to start by creating a Maven pom.xml file. The pom.xml is the recipe that is used to build your project. Open your favorite text editor and add the following:

Maven Dependency for plugins:

<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
       	xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       	xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <groupId>com.wizzdi.examples</groupId>
    <artifactId>example-service</artifactId>
    <version>1.0.0</version>
    <properties>
        <flexicore-api.version>4.0.12</flexicore-api.version>
        <maven.compiler.source>1.8</maven.compiler.source>
        <version.compiler.plugin>3.3</version.compiler.plugin>
        <version.eclipselink>2.7.7</version.eclipselink>
        <maven.compiler.target>1.8</maven.compiler.target>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
        <pf4j-spring.version>0.6.0</pf4j-spring.version>
    </properties>
    <dependencyManagement>
        <dependencies>
            <dependency>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-dependencies</artifactId>
                <version>2.3.0.RELEASE</version>
                <type>pom</type>
                <scope>import</scope>
            </dependency>
        </dependencies>
    </dependencyManagement>
    <dependencies> 
        <dependency>
            <groupId>org.jboss.resteasy</groupId>
            <artifactId>resteasy-spring-boot-starter</artifactId>
            <version>4.0.0.Final</version>
            <scope>provided</scope>
        </dependency>
        <dependency>
            <groupId>com.wizzdi</groupId>
            <artifactId>flexicore-api</artifactId>
            <version>${flexicore-api.version}</version>
            <scope>provided</scope>
        </dependency>
        <dependency>
            <groupId>io.swagger.core.v3</groupId>
            <artifactId>swagger-jaxrs2</artifactId>
            <version>2.0.8</version>
            <scope>provided</scope>
        </dependency>
        <dependency>
            <groupId>org.pf4j</groupId>
            <artifactId>pf4j-spring</artifactId>
            <version>${pf4j-spring.version}</version>
            <scope>provided</scope>
        </dependency>
    </dependencies>
    <build>
        <resources>
            <resource>
                <filtering>true</filtering>
                <directory>src/main/resources</directory>
            </resource>
        </resources>
        <plugins>
            <plugin>
                <artifactId>maven-compiler-plugin</artifactId>
                <version>3.8.1</version>
            </plugin>
            <plugin>
                <artifactId>maven-shade-plugin</artifactId>
                <version>3.2.1</version>
                <executions>
                    <execution>
                        <phase>package</phase>
                        <goals>
                            <goal>shade</goal>
                        </goals>
                        <configuration>
                            <minimizeJar>false</minimizeJar>
                            <createDependencyReducedPom>true</createDependencyReducedPom>
                            <dependencyReducedPomLocation>${java.io.tmpdir}/dependency-reduced-pom.xml
                            </dependencyReducedPomLocation>
                            <transformers>
                                <transformer implementation="org.apache.maven.plugins.shade.resource.ManifestResourceTransformer">
                                    <manifestEntries>
                                        <Plugin-Id>${artifactId}</Plugin-Id>
                                        <Plugin-Version>${version}</Plugin-Version>
                                    </manifestEntries>
                                </transformer>
                            </transformers>
                        </configuration>
                    </execution>
                </executions>
            </plugin>
        </plugins>
    </build>
</project>

 

note that all of these dependencies are provided scoped , thats becuase they exist in your FlexiCore-exec-*.jar 

 

At this point, you could import the project into an IDE (most modern Java IDEs include built-in support for Maven). For simplicity, we continue to use a plain text editor for this example.

 

3.4.2.Writing The Code

To finish our application, we need to create a single Java file. By default, Maven compiles sources from src/main/java, so you need to create that directory structure and then add a file named src/main/java/ExampleRESTService.java to contain the following code:

package com.flexicore.examples.rest;

import com.flexicore.annotations.OperationsInside;
import com.flexicore.annotations.ProtectedREST;
import com.flexicore.annotations.plugins.PluginInfo;
import com.flexicore.interfaces.RestServicePlugin;
import com.flexicore.security.SecurityContext;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import org.pf4j.Extension;
import org.springframework.context.annotation.Primary;
import org.springframework.stereotype.Component;

import javax.ws.rs.*;
import javax.ws.rs.core.Context;
import javax.ws.rs.core.MediaType;

/**
 * Created by Asaf on 04/06/2017.
 */
@PluginInfo(version = 1)
@OperationsInside
@ProtectedREST
@Path("plugins/Example")
@Tag(name = "Example")
@Produces(MediaType.APPLICATION_JSON)
@Consumes(MediaType.APPLICATION_JSON)
@Component
@Extension
@Primary
public class ExampleRESTService implements RestServicePlugin {



    @GET
    @Operation(summary = "hello", description = "hello")
    @Path("hello")
    public String hello(
            @HeaderParam("authenticationKey") String authenticationKey, @Context SecurityContext securityContext) {
        return "world";
    }
}

the class must implement  Plugin interface to be identified by FlexiCore , this class implements RestServicePlugin as it contains Rest Services , usually classes are implementing RestServicePlugin  (for REST), ServicePlugin (for general business logic class) or extending AbstractRepositoryPlugin (for repository)

the annotations in class can be divided into groups:

  1. JAX-RS annotations - annotations for expressing the REST api , more info in JAX-RS docs ( these are the  @GET,@Produces,@Consumes,@Path,@HeaderParam,@Context annotations)
  2. PF4J annotations -   @Extension annotation , this will add this class to PF4J's extensions.idx file.
  3. OpenAPI(Swagger) annotations -  @Tag annotation
  4. Spring annotations -  @Primary,@Component more info in spring docs 
  5. FlexiCore annotations :
    • @PluginInfo(version = 1)   - tells FlexiCore this bean should be loaded as version 1 ( FlexiCore allows running plugins of different versions at the same time, see Same service in multiple versions )
    •  @OperationsInside tells FlexiCore this class contains operations for Access Control
    •  @ProtectedREST tells FlexiCore that rest methods in this class are protected and requires Authentication and authorization
    •  @Operation tells FlexiCore the information for a specific method , will create an operation with the given name and description

3.4.3.Running the Example

at this point your plugin should work . create a jar using  mvn package command and place the output jar into FlexiCore's plugin directory.

if FlexiCore was already running stop it and start it again using :

java -Dloader.main=com.flexicore.init.FlexiCoreApplication -Dloader.path=file:/home/flexicore/entities/ -jar /opt/flexicore/flexicore.jar --spring.config.additional-location=file:///home/flexicore/config/

If you open a web browser to http://localhost:8080/FlexiCore/rest/Example/hello , you should see the following output:

world

To gracefully exit the application, press ctrl-c.

4.Using Flexicore

5.Flexicore Features

5.1. Spring Empowered Plugins

 

 

5.1.1.Introduction

the common solutions when designing a software system are:

  1. Create a monolithic application, use discipline, and patterns to keep your system manageable.
  2. Use micro-services.
    • Micro-services are complex to manage and come with inherent drawbacks related to how they co-work, how the database is designed, maintained, and shared.
  3. plugin-based architecture, like OSGi, etc.

FlexiCore based Architecture

Use FlexiCore to build an entire system from plugins using Spring the way you know and like.

Enjoy a 100% modular system where different, loosely coupled components are used to build an entire system.

While there are solutions for plugins support for a spring boot application using PF4J, none provides the ability to access plugin's services from another plugin.

There were efforts to allow OSGi based system to provide a full web, database solution using Apache Karaf. However, this solution provides little benefits if at all for backend development.

Why inter-injected-plugins is essential for truly modular development:
  • When services need to be used by other services, a non-dependency between plugins solution forces the use of a single plugin for all associated services. This tends to create a plugin that can be as complex and difficult to maintain as a monolithic application.
  •  When plugins cannot be inter-injected, reuse of your or third party plugins is impossible.
How inter-injected-plugins in flexicore provides a solution:

A fundamental advantage available in Flexicore is the ability to inject plugins into other plugins and extend database entities defined in a plugin in another plugin dependent on it directly or indirectly.

This unique ability of Flexicore makes development in Flexicore truly modular while allowing development to reuse and modify existing plugins' services and entities.

This is carried out without breaking the nature of Spring development.

Plugins support dependency injection among them and entity inheritance  among entity plugins, there are no limitations to what can be implemented by plugins and FlexiCore development paradigm is:

Everything is a plugin

 

Flexicore in Mirco-services development.

FlexiCore can be used to build more modular micro-services while reusing plugins across multiple micro-services. This is especially useful if multiple micro-services use the same database.

Flexicore in Mirco-services development

Once the required plugins are created, all the model-plugins (also called entities) should be stored in a pre-configured location (the default is /home/flexicore/entities for Linux).

Deploying Flexicore plugins.

The service-plugins should be stored after build in a pre-configured location (the default is /home/flexicore/plugins for Linux).

Alternatively, plugins update service can be used through the proper APIs, see: Software Updater.

 

5.1.2.Model Plugins

The model is the collection of your entities and external entities (from Flexicore and other plugins) used to describe the persistent classes and their relations.

The model is described in code and it is almost fully identical with standard JPA entities.

When creating the model for a Flexicore application the following should be noted:

  • Check Flexicore documentation and available plugins for existing entities you can extend. Inheritance is a very important paradigm in Flexicore.
  • Decide early on what are the relationships between entities. A many-to-many relationship requires a connecting entity, there are many examples in Flexicore examples and Flexicore itself.
  • Use annotations to create indexes in the relational database so the performance of queries will be largely unrelated to the database size

5.1.2.1.Creating Entities

Entities are created using the same annotations and principles used in standard JPA entities' creation.

See for example:

package com.flexicore.example.person;

import com.flexicore.model.Baseclass;
import com.flexicore.security.SecurityContext;

import javax.persistence.Entity;

@Entity
public class Person extends Baseclass {
    public Person(String name, SecurityContext securityContext) {
        super(name, securityContext);
    }

    public Person() {
    }

    private String firstName;
    private String lastName;

    public String getFirstName() {
        return firstName;
    }

    public <T extends Person> T setFirstName(String firstName) {
        this.firstName = firstName;
        return (T) this;
    }

    public String getLastName() {
        return lastName;
    }

    public <T extends Person> T setLastName(String lastName) {
        this.lastName = lastName;
        return (T) this;
    }
}

Note the constructor and inheritance, in order to enjoy Flexicore Access Control features, all entities extend Baseclass or any of its children. It is pretty much like Object in Java being the ancestor of all classes.

for a full, simple example, checkout the example's Github repository

few notes regarding this example:

  • The persistence.xml in resources folder should have a reference to the entities of this plugin, see below.
  • Services based on these entities reference the entities through a dependency entry in their pom file. follow Creating the POM for more info.
....
<class>com.flexicore.model.Baseclass</class>
<class>com.flexicore.example.person.Person</class>
<class>com.flexicore.example.library.model.Author</class>
<class>com.flexicore.example.library.model.Book</class>
<class>com.flexicore.example.library.model.Subscription</class>


<exclude-unlisted-classes>true</exclude-unlisted-classes>
....

Extending Baseclass

Extending Baseclass is not mandatory and entities can be created as in any Spring boot application. However, some of the more powerful features of Flexicore will not be available. The most notable one is Flexicore high-performance multi-tenancy  Access Control.

Some of the available plugins,  synchronization, for instance, will not work with entities not extending Baseclass directly or indirectly.

5.1.2.2.Dynamic Schema

Entities in the system can have additional fields that are not defined in the schema.

When a dynamic container includes unknown fields, these are saved in the PostgreSQL database as BSON (Binary JSON).

Queries can use these fields and test for equality.

Enabling Dynamic Schema

enabling dynamic schema is easily done by having the object received from the client for the creation of the desired entity extend  BaseclassCreate  and implement the supportingDynamic method below :

@Override
public boolean supportingDynamic() {
    return true;
}

 BaseclassNewService.updateBaseclassNoMerge should be called prior of merging the object

additional data will be saved in  Baseclass.jsonNode  property

Enabling Dynamic Filtering

enabling dynamic schema is easily done by having the filtering object used to fetch the entity extends  FilteringInformationHolder  and implementing the method below :

@Override
public boolean supportingDynamic() {
       return true;
}

Saving Dynamic Properties

when creating a Dynamic Schema enabled entity any unknown JSON properties or nested properties will be saved:

{
    "example": {
        "description": "this is a dynamic object",
        "test":1
    }
}

 

in the above example the field  example (object) will be saved along with other properties for the object.

Filtering by Dynamic Properties

when fetching a dynamic enabled entity, filtering by dynamic properties is done by adding the desired value for desired fields, for example:

{
    "example": {
        "test":1
    }
}

this will return all entities of the requested type which has the value of  test property inside example set to 1.

5.1.2.3.Choosing Persistence Strategy

Flexicore uses two different types of databases.

PostgresSQL is the main database and is used for the Flexicore model and the Entities described in plugins.

MongoDB (no-SQL)  database is used for immutable data only, such as auditing, events searchable logs, health information, etc.

Both databases are supported by the multi-node synchronization plugin. However, Flexicore access control to instances is only provided for the relational database.

Access control to instances on the no-SQL database is normally provided using two steps, find the limiting data in the relational database, for example, user id(s), and filter the no-SQL data using these fields.

Access control to API can be used regardless of the target database used.

Access to NO-SQL is provided through MongoDB Java SDK. No ORM framework is used in conjunction with MongoDB.

 

 

5.1.3.Service Plugins

Services plugins manage everything outside of entities. Services represent all logic in a plugin.

Services can co-reside on the same server at different versions using the same API.

Services can be injected into other services, Injected services are the depend-on services where inject-into services are the dependent services.

 

 

5.1.3.1.Dependency among Service Plugins

FlexiCore can be used to develop full systems based only on plugins. This is possible as services in a plugin A can be injected into plugin B, Plugin B depends on Plugin A.

FlexiCore takes care of injecting Spring beans. The correct version is injected if specified, otherwise the default version is provided.

for example:

package com.flexicore.examples.service;

import com.flexicore.annotations.plugins.PluginInfo;
import com.flexicore.data.jsoncontainers.PaginationResponse;
import com.flexicore.example.library.model.Author;
import com.flexicore.examples.data.AuthorRepository;
import com.flexicore.examples.request.AuthorCreate;
import com.flexicore.examples.request.AuthorFilter;
import com.flexicore.examples.request.AuthorUpdate;
import com.flexicore.interfaces.ServicePlugin;
import com.flexicore.model.Baseclass;
import com.flexicore.security.SecurityContext;
import org.pf4j.Extension;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Primary;
import org.springframework.stereotype.Component;

import java.util.List;
import java.util.logging.Logger;

@PluginInfo(version = 1)
@Component
@Extension
@Primary
public class AuthorService implements ServicePlugin {

    @PluginInfo(version = 1)
    @Autowired
    private AuthorRepository repository;

    @PluginInfo(version = 1)
    @Autowired
    private PersonService personService;

In the code above has two injected Beans, an AuthorRepository instance that is defined in the same jar (in this case plugin jar) the above code is extracted from.

On the other hand, the injected PersonService is from another plugin and is defined in a different jar, FlexiCore takes care of instantiating the required bean and injecting the instance into the AuthorService plugin.

In order to make the person-service plugin available to library-service during compile-time the following steps should be followed:

  1. The dependency definition in the library-service  project is defined:
    <dependency>
        <groupId>com.wizzdi.examples</groupId>
        <artifactId>person-service</artifactId>
        <version>${person-service.version}</version>
        <scope>provided</scope>
    </dependency>

    Note that the scope for this dependency is provided,  that is, the actual compiled code is provided by FlexiCore at run time and it is not packaged into the library-service jar.

     

  2. the manifest of library-service should be updated so the dependency is known at runtime , this is easily done by using pfj4's maven plugin defined in the pom.xml file (note the Plugin-Dependencies tag):
<plugin>
    <artifactId>maven-shade-plugin</artifactId>
    <version>3.2.1</version>
    <executions>
        <execution>
            <phase>package</phase>
            <goals>
                <goal>shade</goal>
            </goals>
            <configuration>
                <minimizeJar>false</minimizeJar>
                <createDependencyReducedPom>true</createDependencyReducedPom>
                <dependencyReducedPomLocation>${java.io.tmpdir}/dependency-reduced-pom.xml
                </dependencyReducedPomLocation>
                <transformers>
                    <transformer implementation="org.apache.maven.plugins.shade.resource.ManifestResourceTransformer">
                        <manifestEntries>
                            <Plugin-Id>${artifactId}</Plugin-Id>
                            <Plugin-Version>${version}</Plugin-Version>
                            <!--suppress UnresolvedMavenProperty -->
                            <Plugin-Dependencies>person-service@&gt;=${person-service.version}
                            </Plugin-Dependencies>
                        </manifestEntries>
                    </transformer>
                </transformers>
            </configuration>
        </execution>
    </executions>
</plugin>

In order to make the person-service plugin available to library-service during runtime, the following steps should be followed:

  1. library-service should be placed in the plugins folder
  2. person-service should be placed in the plugins folder
  3. any other plugin that library-service or person-service depend on should be placed in the plugins folder
  4. any entity-based plugin that library-service or person-service depend on should be placed in the entities folder

In Linux, this is by default: /home/flexicore/plugins and is defined in Spring application.properties  file.

 

 

 

 

5.1.3.2.REST Service in Service Plugins

A service plugin may include API endpoints. The definition is similar to normal REST endpoint definitions in Spring with some additional fields required for the system persistence based access-control.

For example, from Flexicore examples

 

package com.flexicore.examples.rest;

import com.flexicore.annotations.OperationsInside;
import com.flexicore.annotations.ProtectedREST;
import com.flexicore.annotations.plugins.PluginInfo;
import com.flexicore.data.jsoncontainers.PaginationResponse;
import com.flexicore.example.library.model.Author;
import com.flexicore.examples.request.AuthorCreate;
import com.flexicore.examples.request.AuthorFilter;
import com.flexicore.examples.request.AuthorUpdate;
import com.flexicore.examples.service.AuthorService;
import com.flexicore.interfaces.RestServicePlugin;
import com.flexicore.security.SecurityContext;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import javax.ws.rs.*;
import javax.ws.rs.core.Context;
import javax.ws.rs.core.MediaType;
import org.pf4j.Extension;
import org.springframework.context.annotation.Primary;
import org.springframework.stereotype.Component;
import org.springframework.beans.factory.annotation.Autowired;


@PluginInfo(version = 1)
@OperationsInside
@ProtectedREST
@Path("plugins/Author")
@Tag(name = "Author")
@Produces(MediaType.APPLICATION_JSON)
@Consumes(MediaType.APPLICATION_JSON)
@Component
@Extension
@Primary
public class AuthorRESTService implements RestServicePlugin {

    @PluginInfo(version = 1)
    @Autowired
    private AuthorService authorService;

    @POST
    @Path("createAuthor")
    @Operation(summary = "createAuthor", description = "Creates Author")
    public Author createAuthor(
            @HeaderParam("authenticationKey") String authenticationKey,
            AuthorCreate authorCreate, @Context SecurityContext securityContext) {
        authorService.validate(authorCreate, securityContext);
        return authorService.createAuthor(authorCreate, securityContext);
    }

The RestServicePlugin

Every class containing REST service endpoints should implement RestServicePlugin.

API endpoints defined in such class are added to the list of REST endpoints provided by Flexicore itself and to these defined in other plugins and in other classes implementing this interface.

Flexicore Specific annotations on the class

  • @PluginInfo(version = 1) 
    • set the version of this class, multiple versions of the same API can co-exist in the server if versions are different. The client code (JS, TS etc.) can specify the preferred version, this version will be selected by the system for this request.
  • @OperationsInside
    • Instructs Flexicore to search for new Operations in this class when initializing, operations are stored in the database and used for access control. For example, this class includes a definition of the createAuthor operation. Operation annotation used here is from Swagger and is used by Flexicore. Swagger can be used for the documentation of the API and for out of the box display of available endpoints.
  • @ProtectedREST
    • Set all endpoints in this class to be protected, users must be signed-in in order to access any endpoint defined here. A SecurityContext object is passed and the access control system is associated with both the Operation and the data it creates/updates or views.  This annotation must be used on licensed features too.
  • @Extension
    • This is not Flexicore annotation. This is PF4J annotation and defines the class as a plugin. The Spring implementation of Flexicore uses PF4J.

SecurtiyContext and authentication key

Each protected endpoint must have as first header parameter the authentication key obtained when the user has signed-in.

@HeaderParam("authenticationKey") String authenticationKey,

The SecurtiyContext must be present too, this is injected by the system and is used in inferring user rights to access this endpoint. The SecurityContext instance is part of the parameters passed downstream so access to data can use it when executing the desired operation.

@Context SecurityContext securityContext

5.1.3.3.Business Logic in Service Plugins

Business flow services are classes implementing the ServicePlugin interface.

Methods in such classes are usually called from RestServicePlugin and are provided with a SecurityContext instance so access control can properly function.

In most cases, a container instance is passed for a create or update operations and a filter instance is passed for aggregation and listing operations.

Business logic should be put here (recommended), data access operations dealing with entities required for a business logic operation should call a data repository defined in this or another plugin.

See below a service class from Flexicore examples

 

package com.flexicore.examples.service;

import com.flexicore.annotations.plugins.PluginInfo;
import com.flexicore.data.jsoncontainers.PaginationResponse;
import com.flexicore.example.library.model.Author;
import com.flexicore.examples.data.AuthorRepository;
import com.flexicore.examples.request.AuthorCreate;
import com.flexicore.examples.request.AuthorFilter;
import com.flexicore.examples.request.AuthorUpdate;
import com.flexicore.interfaces.ServicePlugin;
import com.flexicore.model.Baseclass;
import com.flexicore.security.SecurityContext;
import org.pf4j.Extension;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Primary;
import org.springframework.stereotype.Component;

import java.util.List;
import java.util.logging.Logger;

@PluginInfo(version = 1)
@Component
@Extension
@Primary
public class AuthorService implements ServicePlugin {

    @PluginInfo(version = 1)
    @Autowired
    private AuthorRepository repository;

    @PluginInfo(version = 1)
    @Autowired
    private PersonService personService;

    public Author createAuthor(AuthorCreate authorCreate,
            SecurityContext securityContext) {
        Author author = createAuthorNoMerge(authorCreate, securityContext);
        repository.merge(author);
        return author;
    }

    public Author createAuthorNoMerge(AuthorCreate authorCreate,
            SecurityContext securityContext) {
        Author author = new Author(authorCreate.getFirstName(),securityContext);
        updateAuthorNoMerge(author, authorCreate);
        return author;
    }

    public boolean updateAuthorNoMerge(Author author, AuthorCreate authorCreate) {
        boolean update =personService.updatePersonNoMerge(author,authorCreate);

        return update;
    }

    public Author updateAuthor(AuthorUpdate authorUpdate,
            SecurityContext securityContext) {
        Author author = authorUpdate.getAuthor();
        if (updateAuthorNoMerge(author, authorUpdate)) {
            repository.merge(author);
        }
        return author;
    }

    public <T extends Baseclass> T getByIdOrNull(String id, Class<T> c,
            List<String> batchString, SecurityContext securityContext) {
        return repository.getByIdOrNull(id, c, batchString, securityContext);
    }

    public PaginationResponse<Author> getAllAuthors(AuthorFilter authorFilter,
            SecurityContext securityContext) {
        List<Author> list = listAllAuthors(authorFilter, securityContext);
        long count = repository.countAllAuthors(authorFilter, securityContext);
        return new PaginationResponse<>(list, authorFilter, count);
    }

    public List<Author> listAllAuthors(AuthorFilter authorFilter,
            SecurityContext securityContext) {
        return repository.listAllAuthors(authorFilter, securityContext);
    }

    public void validate(AuthorFilter authorFilter,
            SecurityContext securityContext) {

    }

    public void validate(AuthorCreate authorCreate,
            SecurityContext securityContext) {

    }
}

Notes

  • Line 21 defines the service version, if a different plugin defines the same class with a different version number, the system will provide both, in such case the API plugin of each version is supposed to call the correct service, that is, from the client specifying a different version of the API to the server the execution chain is different. It is assumed that changes to the model (entities) if any, support all active service versions. The persistence (database) is always at the latest version and is expected to be backward compatible.
  • Line 29 injects the AuthorRepository. The version is specified, it may be defined in a different plugin and will be properly injected depending on the version.
  • Line 33 injects a PersonService, the version is also defined and is provided by a plugin matching the version.

 

 

5.1.3.4.Data Repositories

The data repository class is responsible for interacting directly with the data. This is a recommended and not a mandatory structure.

The actual instantiation of classes in the example is carried out at the service level, however, the service calls the data repository to update or query the database.

Such a design pattern allows easy replacement of the actual data handling using SQL statements (unrecommended) or even a NO-SQL database.

In Flexicore data repositories need to extend the AbstractRepositoryPlugin class.

The code below is an example of such a class taken from the examples repository in Github.

The AbstractRepositoryPlugin  provides a set of data access methods as depicted in the system Javadocs.

package com.flexicore.examples.data;

import com.flexicore.annotations.plugins.PluginInfo;
import com.flexicore.example.library.model.Author;
import com.flexicore.examples.request.AuthorFilter;
import com.flexicore.interfaces.AbstractRepositoryPlugin;
import com.flexicore.model.QueryInformationHolder;
import com.flexicore.security.SecurityContext;
import org.pf4j.Extension;
import org.springframework.stereotype.Component;

import javax.persistence.criteria.CriteriaBuilder;
import javax.persistence.criteria.CriteriaQuery;
import javax.persistence.criteria.Predicate;
import javax.persistence.criteria.Root;
import java.util.ArrayList;
import java.util.List;

@PluginInfo(version = 1)
@Extension
@Component
public class AuthorRepository extends AbstractRepositoryPlugin {

    public List<Author> listAllAuthors(AuthorFilter filtering,
            SecurityContext securityContext) {
        CriteriaBuilder cb = em.getCriteriaBuilder();
        CriteriaQuery<Author> q = cb.createQuery(Author.class);
        Root<Author> r = q.from(Author.class);
        List<Predicate> preds = new ArrayList<>();
        addAuthorPredicate(filtering, cb, r, preds);
        QueryInformationHolder<Author> queryInformationHolder = new QueryInformationHolder<>(
                filtering, Author.class, securityContext);
        return getAllFiltered(queryInformationHolder, preds, cb, q, r);
    }

    private void addAuthorPredicate(AuthorFilter filtering, CriteriaBuilder cb,
            Root<Author> r, List<Predicate> preds) {
        PersonRepository.addPersonPredicate(filtering, cb, r, preds);

    }

    public Long countAllAuthors(AuthorFilter filtering,
            SecurityContext securityContext) {
        CriteriaBuilder cb = em.getCriteriaBuilder();
        CriteriaQuery<Long> q = cb.createQuery(Long.class);
        Root<Author> r = q.from(Author.class);
        List<Predicate> preds = new ArrayList<>();
        addAuthorPredicate(filtering, cb, r, preds);
        QueryInformationHolder<Author> queryInformationHolder = new QueryInformationHolder<>(
                filtering, Author.class, securityContext);
        return countAllFiltered(queryInformationHolder, preds, cb, q, r);
    }

}

Notes:

  • The listAllAuthors  method is an example of getting a filtered list of something that extends Baseclass in this case, the caller has passed an instance of AuthorFilter that extends a FilteringInformationHolder.
  • The filter is used to create a list of predicates passed to the CriteriaQuery.
  • The returned result is of type PaginationResponse<Author>, it is strongly recommended to avoid getting all instances of something in one call, Flexicore provides the required filtering and response types supporting pages.

5.1.3.5.Dynamic Service Lookup

When plugin A is extended by an unknown number of plugins, for example when a defrayment service may use an unknown number of providers, such as credit card charging services, the flow will be:

the defrayment service plugin A provides such services regardless of the provider, for example in a checkout service.

Plugin A can be injected with any number of Interface X (Java interface). Interface X itself is defined in plugin A or in any service Plugin A depends on.

Any plugin designed to be used by Plugin A should :

  • Depend on plugin A
  • implement Interface X

The framework injects all beans implementing Interface X into Plugin A. Plugin A has no dependency on any Interface X implementors and it can provide if required, a list of such implementors.

 

5.1.3.6.Multi-Versioned Service Plugins

5.2.Access Control

Flexicore access control supports multi-tenancy, uses the database for all settings and definition,  that is, it doesn't rely on hardcoded policies.

Flexicore access control system offers very fine granularity access control to data along with powerful default behavior policies for maximum efficiency of permissions persistence.

When entities are created in plugins support for Flexicore access control is implicitly provided. This is true if entities are created following the inheritance rules.

5.2.1.Multi-Tenancy

Introduction

Flexicore system is designed to provide a very flexible and powerful multitenancy access control system. 

Multitenancy is required when a server is built to provide services to businesses or to a group of users managed as such.

The access control system restricts access to API calls, internal calls, and the data the system manages. The system is designed to avoid any hard-coded roles and policies, instead, access control is fully defined in the database. The only code association is through the definition of the Operation annotation.

Instances of the Operation class are stored in the database when the system starts, if not previously stored

Access to the API can be managed in much higher granularity.

When new Operation instances defined in plugins they are stored in the database. The accumulated list of all Operations from plugins and Flexicore itself is available through the system API and the system generic management console.

In order to reduce the number of database rows created to store permissions and policies, the system uses a default set of policies. Default behaviors can easily be defined through the APIs or through the provided management console. When not defined, the system imposes its own default behaviors.

A set of API calls is provided for managing access control related entities in the system.

Instances of entities defined in plugins get full access control as long as they are defined to extend the Baselclass class from flexicore.api or any entity that has Baseclass as a superclass, directly or indirectly.

For more information about multi-tenancy see multitenancy

5.2.2.Tenants, Role and Users

 

 

Flexicore access control system makes use of the following Entities:

Tenant

A tenant Defines an isolated dataset that can be regarded as a separate database, when a Tenant is created, the administrator of the tenant is created too.

The Administrator of a tenant can create new tenants and create new administrators for any tenant he or she controls.

The permissions system can be used to block the new tenant creation by a tenant administrator.

Important points to know about tenants:

  • Any instance of any entity has a Tenant field, this is true for all entities directly or indirect;y  extending  Baseclass .
  • Users have a default tenant.
  • The tenant field of any instance is the default tenant of the user creating the instance. This includes instances created indirectly by system objects such as DynamicExecution instances.
  • Users can be associated with more than a single tenant and view data across many tenants based on the access control policies defined.
  • Roles (see below) belong to a single tenant.
  • When the system is created, a tenant called defaulttenant is created along with a super administrator.
  • Users of a tenant can be granted access to any piece of data in the system regardless of their default tenant.
  • System objects required by more than one tenant can be grouped in a PermissionGroup see below and permitted at the tenant level.

Role

The role entity groups multiple users so permissions can be granted to all members of the role.

Users can be members of multiple roles. The more permissive policy applies. For example, if a user is a member of the viewer role and of the administrator role and viewers cannot edit an instance while administrators can, the user will be permitted to edit the said instance.

Roles are defined per tenant.

User

Usually describes a person, users are identified through two unique identifiers, their email and the id the system created (GUID) when the user is created. Users have default tenant but can access multiple tenants if so permitted.

 

 

 

5.2.3.Operations and Type-Based Access Control

5.2.4.API Access control

API calls access control:

REST API calls are invoked from a client are usually annotated with the Operation annotation. When the server starts, it adds these operations to the database, if not already added, so the REST method is now defined in the database and can be used for access rights. Access control on API calls permits or blocks an Operation without checking what are the Objects to be affected or fetched. Checking the instances access right is always performed only when the operation itself is allowed.

When simple CRUD operations are needed, developers can use built-in operations instead of defining new ones, these are Read, Write, Delete, and Update.  Using custom Operations is required when a finer granularity of access control is needed.

Invokers, methods, and Dynamic Executions.

Flexicore includes a powerful generic system for internal server methods invocations called Invokers. Invokers are used with Flexicore rules engine, scheduling, and with Flexicore generic user interface support. When a Dynamic Execution is created (a combination of Invoker, method, parameters/filters ) the creator access rights are used when the Dynamic Execution needs to run. The set of access rules apply in the same way these are applied to REST endpoints operations.

Flow

When a REST call is invoked, the system checks the following:

  1. Checks if this Operation is allowed for the current user. If yes, proceed
  2. If not check if this operation is denied for the current user, if yes, block the operation.
  3. Check if Operation permitted for any of the Roles the current user belongs to, if yes, proceed.
  4. Check if Operation is denied for any role the user belongs to, if yes, block the operation.
  5. At this point, the system checks the default behavior as follows:
  6. Check if the current Tenant (the Tenant the user is logged into) is allowed by default to perform the operation, if yes, proceed to perform the operation.

That is, whether all tenants members can perform the operation if an explicit allow or deny wasn’t defined directly on the user or any role the user belongs to.

  • Check if the current Tenant is denied by default to perform the operation if yes, block the operation.
  • If none of the above tests has ended with a proceed or block, check if the operation was allowed or denied by the default behavior for the operation, this is defined in the code.

The diagram below depicts the sequence of operations when deciding if an API call is permitted.

The same flow is applied when Dynamic Executions are executed.

Note: as performance is a key target for us, the above is executed in one database operation

The flow of Access Control rules on Operations (REST, Dynamic Executions)

5.2.5.Data Access control

 

Instance access control is implemented through a SecurityLink between a User, a Role, or a Tenant with the instance/class in question and the operation to be performed.

Such a link connects to instances (in practice, rows in the database) and  has two additional fields as follows: A complex value is an instance of Operation type and the simple value(String) is either ‘Deny’ or ‘Allow’

A SecurityLink defines the right to access or the lack of it (denial) in the following possible scenarios:

  • Between a User and an instance of an object (any object).
  • Between a Role and an instance of an object, thus saving the need to define permission for users associated with a Role (users can be members of multiple roles). Access to the object will be permitted if any of the roles the user is associated with is permitted to access the instance.
  • Between a Tenant and instance of the Clazz type, every instance in the system is associated with a type, types are stored in the database in a table called Clazz. When a tenant is associated with permission with an instance of a Clazz, it practically defines the default access behavior for users of this tenant for this type(Clazz) for this Operation. For example, The administrator of the Tenant is likely to have special permission (or the Administrators Role) as described above to override the default tenant behavior and to allow Administrators to manage users. Note that this is different for granting access to the Operation as described above, here a certain User (can be part of a Role called Administrators or any other name) was granted access to specific instances of the User type, so for example, a user who is the manager of the organization can be made blocked to other users even if they belong to a Role who was granted access to the User  Clazz.

Note that a SecurityLink is always associated with an Operation, so for defining the default behavior for hundred instances of operations (the system may have many such operations in addition to the normal 4 -read, write, delete, update) hundred permissions between the tenant and each type of instance (Clazz) must be defined, otherwise, users will see only objects they have created. Therefore, the system may define default behavior for any operation, and for the matter also for any clazz.

If such a security link is defined in a tenant with the ‘Allow’ attribute, it implies that unless explicitly defined otherwise all instances in the tenant are accessible to all users.

The details:

  1. Any instance is accessible for any Operation in context if the logged-in user is the creator of the instance.
  2. Otherwise, an instance is accessible by Operation in context to all users that have explicit permission on that instance with an ‘allow’ value. If such permission exists with a ‘deny’, access to that object will be blocked.
  3. In case no explicit permission exists for the current user, permission between any Role the user belongs to is looked for. If such permission exists, it behaves in a similar way to the behavior of User permission. If the permission has an ‘allow’ value, the operation will be allowed on this instance, if a ‘deny’ value is defined, the instance will not be accessed and further tests are terminated (for this instance).
  4. In case no explicit permission exists for any of the Roles of the current User, permission between any such Role the type (Clazz) of the instance is looked for. If such permission exists, access is either blocked or granted based on the value 'deny, if a ‘deny’ value is defined, the instance will not be accessed and further tests are terminated (for this instance).
  • The last tests for this instance are related to the default behavior for this Tenant and the Operation in question. This is tested for the type or class that the Operation needs to access (for example list all Users in the system is associated with the type User). If a permission link exists between the Tenant and the Clazz in question (that is, the Type in context), the value in that link dictates the access right to all instances of this type for the Operation in context. An ‘Allow’ value will grant access while a ‘Deny’ value will block it. Overriding this default Tenant behavior is done through the Role and User Permissions described above.

 

5.2.5.1.Permission Groups

Permission groups were created to allow multiple instances to be managed, permission-wise, as one.

In a way, permission groups are symmetrical in concept to Roles. Roles are there to facilitate the same behavior to a group of Users. Permission Groups allow the same behavior to a group of instances.

Properties of Permission Groups.

  • A permission group can contain instances of any type.
  • Instances can be members of multiple permission groups.
  • When a SecurityEntity (Tenant, Role, User) has permission to some operation on a permission group instance, it is similar to having specific permission of such operation on any instance included in the permission group.
  • Instances in a permission group must extend Baseclass either directly or indirectly.

See relevant APIs and the system provided user interface for managing permissions.

5.2.6.Access control UI support

5.2.7.Queries and Filters

when you need to fetch access control filtered information from the database you will need to create a repository which extends AbstractRepositoryPlugin and call getAllFilteredcountAllFiltered or prepareQuery for more advanced cases .these receive a FilteringInformationHolder or extenders and an optional  SecurityContext if not provided no access control filters will be done . additional filtering may be added using JPA's Criteria API.

SecurityContext is usually obtained from REST api's but can also be obtained directly from  SecurityService

FilteringInformationHolder Comes out of the box with additional filters which are enforced when getAllFilteredcountAllFiltered or prepareQuery are called.

starting a criteria api query from Author

CriteriaBuilder cb = em.getCriteriaBuilder();
CriteriaQuery<Author> q = cb.createQuery(Author.class);
Root<Author> r = q.from(Author.class);

adding additional filters( simple predicate)

if(filtering.getNames()!=null&&!filtering.getNames().isEmpty()){
   preds.add(r.get(Author_.name).in(filtering.getNames()));
}

a more advanced filtering including join:

if(filtering.getBooks()!=null&&!filtering.getBooks().isEmpty()){
    Set<String> ids=filtering.getBooks().stream().map(f->f.getId()).collect(Collectors.toSet());
    Join<Author, Book> join=r.join(Author_.books);
    preds.add(join.get(Book_.id).in(ids));
}

calling getAllFiltered , constructing the required QueryInformationHolder and providing SecurityContext

QueryInformationHolder<Author> queryInformationHolder = new QueryInformationHolder<>( filtering, Author.class, securityContext);
return getAllFiltered(queryInformationHolder, preds, cb, q, r);

here is the full example repository:

@PluginInfo(version = 1)
@Extension
@Component
public class AuthorRepository extends AbstractRepositoryPlugin {

   public List<Author> listAllAuthors(AuthorFilter filtering,
         SecurityContext securityContext) {
      CriteriaBuilder cb = em.getCriteriaBuilder();
      CriteriaQuery<Author> q = cb.createQuery(Author.class);
      Root<Author> r = q.from(Author.class);
      List<Predicate> preds = new ArrayList<>();
      addAuthorPredicate(filtering, cb, r, preds);
      QueryInformationHolder<Author> queryInformationHolder = new QueryInformationHolder<>(
            filtering, Author.class, securityContext);
      return getAllFiltered(queryInformationHolder, preds, cb, q, r);
   }

   private void addAuthorPredicate(AuthorFilter filtering, CriteriaBuilder cb,
         Root<Author> r, List<Predicate> preds) {
//adding person filters , since AuthorFilter inherites from PersonFilter
      IPersonRepository.addPersonPredicate(filtering, cb, r, preds);
//filter authors by given names
      if(filtering.getNames()!=null&&!filtering.getNames().isEmpty()){
         preds.add(r.get(Author_.name).in(filtering.getNames()));
      }
//filter author by specific books
      if(filtering.getBooks()!=null&&!filtering.getBooks().isEmpty()){
         Set<String> ids=filtering.getBooks().stream().map(f->f.getId()).collect(Collectors.toSet());
         Join<Author, Book> join=r.join(Author_.books);
         preds.add(join.get(Book_.id).in(ids));
      }

   }

   public Long countAllAuthors(AuthorFilter filtering,
         SecurityContext securityContext) {
      CriteriaBuilder cb = em.getCriteriaBuilder();
      CriteriaQuery<Long> q = cb.createQuery(Long.class);
      Root<Author> r = q.from(Author.class);
      List<Predicate> preds = new ArrayList<>();
      addAuthorPredicate(filtering, cb, r, preds);
      QueryInformationHolder<Author> queryInformationHolder = new QueryInformationHolder<>(
            filtering, Author.class, securityContext);
      return countAllFiltered(queryInformationHolder, preds, cb, q, r);
   }

 

5.2.8.Access Control Management System

5.3.Dynamic invokers

Dynamic Invokers are services which can be executed through a known rest API endpoint, FlexiCore allows to enumerate dynamic invokers methods and parameters allowing the caller to implement a generic code for executing them it is in concept similar to GraphQL

dynamic invokers are useful when we would like to support multiple services with the same logic without requiring a client-side change.

1.CoffeeMakerA

2.CoffeeMakerB

and assuming we would like each to implement a "makeCoffee" service :

Using REST API

we can create two rest APIs for each one  /rest/plugins/CoffeeMakerA/makeCoffee and  /rest/plugins/CoffeeMakerB/makeCoffee

the issue here is that our client has to be aware of CoffeeMakerA and CoffeeMakerB , so once we add a new coffee marker client code will have to change

Using Java Interfaces

another solution will be to have all coffeeMakers implement an interface:

public interface CoffeeMaker {
    
    void makeCoffee(MakeCoffeeRequest makeCoffeeRequest);
}

and create a single rest API in the plugin defining that interface /rest/plugins/CoffeeMaker/makeCoffeethis rest api will go over all coffeeMakers get the required coffee maker which is one of the parameters for MakeCoffeeRequest .

the issue here is that the client cant use a different parameter for each coffee marker if it is required or has to be aware of a CoffeeMaker specific parameters which will again require client-side code

Using Invokers

each plugin will implement an invoker with a method called makeCoffee , a common object may or may not be used to describe the request, client-side will call /rest/DynamicInvokers/getAllInvokers to get the available invokers in the system taking only those implementing makeCoffee method allowing the user to select the relevant one and setting the parameters for it. if a new CoffeeMarkerC will be added no change is required on client-side

 

Implementing an Invoker

invoker is a Service Plugin which implements the  Invoker interface and contains methods annotated with  @InvokerMethodInfo

here is our  CoffeeMakerAInvoker  class

package com.flexicore.examples.service;

import com.flexicore.annotations.plugins.PluginInfo;
import com.flexicore.interfaces.dynamic.Invoker;
import com.flexicore.interfaces.dynamic.InvokerMethodInfo;
import com.flexicore.security.SecurityContext;
import org.pf4j.Extension;
import org.springframework.stereotype.Component;

@PluginInfo(version = 1)
@Extension
@Component
public class CoffeeMakerAInvoker implements Invoker {

    @InvokerMethodInfo(displayName = "makeCoffee", description = "Makes coffee using Coffee Maker's A Service", relatedClasses = {Coffee.class})
    public Coffee makeCoffee(MakeCoffeeRequest makeCoffeeRequest,SecurityContext securityContext) {
      // make coffee logic
    }
    @Override
    public Class<?> getHandlingClass() {
        return Coffee.class;
    }
}

ListingInvoker

extends the invoker interface requiring the implementor to implement a listAll method and give a filter class type

@InvokerMethodInfo contains fields for better describing the method to the client such as displayName,description and relatedClasses

to allow the client to show all relevant parameters for the makeCoffee method we need to annotate MakeCoffeeRequestfields with matching annotations:

@FieldInfo - basic field, allows setting name,description,mandatory(true or false),defaultValue,validation(string)

@IdRefFieldInfo - a string field or a list of strings which references a specific type (this allows client side to use another invoker to list possible values for that type) allows setting displayName,Description,mandatory(true or false),list(true or false if this field is of type string or list of strings),refType(the given ids are the types of this class)

@ListFieldInfo -same as FieldInfo but for a list , this is different from @IdRefFieldInfo since the values in the list are not ids of a different entity

here is our MakeCoffeeRequest class:

package com.flexicore.examples.service;

import com.flexicore.interfaces.dynamic.FieldInfo;
import com.flexicore.interfaces.dynamic.IdRefFieldInfo;
import com.flexicore.interfaces.dynamic.ListFieldInfo;

public class MakeCoffeeRequest {
    @FieldInfo(displayName = "Customer Name",description = "Customer requesting coffee",mandatory = true,defaultValue = "John Doe")
    private String customerName;
    @IdRefFieldInfo(displayName = "Coffee Machine",description = "The Coffee Machine that will be used to make the coffee",refType = CoffeeMachine.class,list = false)
    private String coffeeMachineId;
    @ListFieldInfo(displayName = "Extra Ingredients",description = "Extra Ingredients to add to coffee",listType =Ingredient.class )
    private List<Ingredient> extraIngredients;

    public String getCustomerName() {
        return customerName;
    }

    public <T extends MakeCoffeeRequest> T setCustomerName(String customerName) {
        this.customerName = customerName;
        return (T) this;
    }

    public String getCoffeeMachineId() {
        return coffeeMachineId;
    }

    public <T extends MakeCoffeeRequest> T setCoffeeMachineId(String coffeeMachineId) {
        this.coffeeMachineId = coffeeMachineId;
        return (T) this;
    }
}

MakeCoffeeRequest should be registered when context is started , note that request objects that are used in a  listAll method are registered with their respective filters

@PluginInfo(version = 1)
@Extension
@Component
public class Config implements ServicePlugin {


    @EventListener
    public void init(PluginsLoadedEvent e) {
        BaseclassService.registerFilterClass(CoffeeMachineFilter.class, CoffeeMahcine.class);
        CrossLoaderResolver.registerClass(MakeCoffeeRequest.class);


    }

}

Common Client Side Sequence

  1. client side will call /rest/DynamicInvokers/getAllInvokers  the response will contain a list of invoker classes ,thier methods and the parameters for each method
  2. client side will allow the user to select/automatically select using some logic the relevant invoker(s) and a method
  3. client side will present the parameters and allow the user to input values, when a field is referencing another entity (the parameter has a non null  idRefType ) a selection will be shown from the result of the listAll invoker method for that given type (in our example when handling the field coffeeMachineId clients side will attempt to find an invoker which has a  handlingType of com.flexicore.examples.service.CoffeeMachine with a  listAll method)
  4. client side will call  /rest/dynamicInvokers/executeInvoker providing:
    •  invokerNames - canonical names for the relevant invoker classes obtained from phase 1 (in our example its  com.flexicore.examples.service.CoffeeMakerAInvoker )
    •  invokerMethodName - (the method on the invoker(s) to call , in our example its  makeCoffee )
    •  executionParametersHolder - the parameters for the method including a  type field with the canonical name of the request object (in our example its  com.flexicore.examples.service.MakeCoffeeRequest )

Dynamic Execution

dynamic execution is an entity holding a specific execution parameters for invoker(s) method , this enable the re-use of that action dynamic execution is widely used in Rules Engine,Grid preset and Scheduling

Creating DynamicExecution

/rest/dynamicInvokers/createDynamicExecution is the endpoint for creating a dynamic execution which expects the same as the executeInvoker api discussed in the previous paragraph and an additional name and description parameters to allow identifying the dynamic execution object.

/rest/dynamicInvokers/updateDynamicExecutionis the endpoint for updating a dynamic execution

the executionParametersHolder field holding the parameters must be a jpa entity following the jpa guidelines which is defined in the Domain model

5.4.File management support

FlexiCore provides a File Management system out of the box. this component revolves around  FileResource entity which most notably has the following fields:

  1.  md5 - the file md5
  2.  offset - the file resource current offset
  3.  done - true/false if the file upload was completed
  4.  keepUntil - if set FlexiCore will delete this file once keepUntil date has passed
  5.  originalFilename - the original file name the file had before being uploaded( flexicore creates a random name for the actual file in the file system)

here are some important APIs:

Upload

  1.  FlexiCore/rest/resources/{md5}  will return a file resource object with the given md5 or empty response if non exists
  2. FlexiCore/rest/resources/upload supports chunk upload and receives binary (octet-stream) body that will be written from the current offset of the file resource and headers:
    •  md5 client-side calculated md5 of the entire file
    •  name filename ( this will be saved as originalName on FileResource
    •  chunkMd5 the current chunk md5 (optional - will not be enforced if not sent), this is available for optional retry on chunk if the communication link is unreliable or slow.
    •  lastChunk - Boolean specifying if this is the last chunk or not, if it is total file md5 will be validated and the file will be closed for changes

Sequence for uploading files

  1. calculate the local file md5
  2. call FlexiCore/rest/resources/{md5} to get the current file offset, if the offset equals the file size, there is no need to upload it.
  3. read a chunk from the offset from the local file, the chunk size is arbitrary and can be selected to best-fit link bandwidth and reliability - note that most http servers set a limit to the request size , the chunk size should not exceed this limit.
  4. calculate chunk md5 ( optional , if chunk md5 is used)
  5. upload chunk calling FlexiCore/rest/resources/upload API, update offset counter from returned FileResource
  6. repeat 3-5 until completion ( setting lastChunk to true for the last chunk)

the upload sequence supports pausing/resuming uploads and makes sure that a completed file always has the correct md5

 

Download

FlexiCore/rest/download/{authenticationkey}/{id} - sends binary content of the file with id using authenticationKey provided , this API is secured by permissions

FlexiCore/rest/downloadUnsecure/{authenticationkey}/{id} - sends binary content of the file with id, this API is NOT secured by permissions, the whole access to the API can be disabled using the Access Control Management System

 

FileResourceService

it is possible to create FileResource objects from FlexiCore plugins, simply  @Autowired  FileResourceService to use its CRUD methods (  createDontPersist,createNoMerge,create,listAllFileResources,updateFileResourceNoMerge and more).

 

 

 

5.5.Authentication and Authorization

FlexiCore has an authentication and authorization support out of the box. Successful authentication will result in an authentication key.

every valid authentication key received by a secure API (REST or WebSocket) results in a  SecurityContext object which can be used by plugins services to obtain relevant/allowed entities for the user by providing the SecurityContext to the  getAllFiltered method in the Access Control section. SecurityContext is also used to deny/allow access for a specific user/role/tenant for a specific operation .

let us assume we have a  getAllPerson service which returns all the persons the user allowed to see. A failed operation access control will result in a 403 forbidden HTTP response. A successful data access control may result in an empty list if the user does not have access to any person entity, this as explained in the Access Control section is supported out of the box by FlexiCore.

Obtaining the Authentication Token

obtaining authentication key by calling endpoint /FlexiCore/rest/authenticationNew/login  providing email or phone number and a mandatory password, the API will return a token with its expiration date

 

Using the Authentication Token

most FlexiCore APIs expect the authentication token to be provided as a header named  authenticationKey

 

Writing a Secure REST API

  1. REST API class must be annotated with  @ProtectedREST
  2. the method must be annotated with FlexiCore's @Operation annotation or FlexiCore's @Read,@Write,@Update,@Delete
  3. method first parameter must be a string representing the authentication token
  4. methods last parameter must be of type SecurityContext annotated by  @Context

an example can be found in Writing The Code section

 SecurityService Programmatic interface

occasionally we would like to get a security context for a specific user in a plugin running code, FlexiCore Allows us doing that by calling  SecurityService.getUserSecurityContextByEmail(userEmail)  or  SecurityService.getUserSecurityContext(user) if the full user object is available

 UserService Programmatic interface

occasionally we would like to get an autheticationToken for a specific user in a plugin running code, FlexiCore Allows us doing that by calling  UserService.registerUserIntoSystem(user)  or UserService.registerUserIntoSystem(user,expirationDate) this allows the plugin author to create a custom authentication mechanism.

5.6.Auditing

Listing Auditing Events

The endpoint for listing auditing events is:  /FlexiCore/rest/audit/getAllAuditingEvents it allows filtering auditing events by operation (list of operation ids), Users (list of user ids), and a date range. See the AuditingFiltering class.

Enabling Auditing for Operation

calling:/FlexiCore/rest/operations/setOperationAuditable/{id} providing the id for the operation to be audited

listing operations is possible using the endpoint /FlexiCore/rest/operations/listAllOperations

Custom Auditing Events

by default FlexiCore Audits the request for the operation and the calling user, for auditing the response registering an auditing container is required:

AuditingService.registerAuditingContainer(canonicalClassName,auditingContainerFactory)

 

5.7.Available plugins

FlexiCore provides a set of plugins out of the box which expands the capabilities of the system.

5.7.1.IoT

The IoT Service supports sending/receiving messages to/from a remote FlexiCore server.

this section refers to the following software components:

  1. flexicore-iot-service
  2. flexicore-iot-model

Concepts

FlexiCore address the issue of IoT system where you have multiple remote IoT devices required to connect to a "Cloud" FlexiCore Server where those devices might not be accessible directly (since they are behind a router) by proposing the following solution:

  1. IoT device is a Linux or Windows based device.
  2. IoT device follows the System Requirements.
  3. IoT device runs FlexiCore.
  4. IoT device runs any configuration of the services in the "Cloud" and all of the entities in the "Cloud". Some of the IoT device services can be omitted from the cloud and vice-versa.
  5. IoT device is configured to connect to the "Cloud" FlexiCore Server - solving the NAT issue.

note that sometimes an IOT device does not have the adequate computing power to support the above requirements (like embedded devices and sensors) or the IoT device needs to run on battery for a long period , in such a cases it is recommended to use a gateway device which will addresses the above requirements and implements a connector that receives the data from the unsupported device on local communication using any porotocl.

As Flexicore hardware requirements are quite modest and it runs on small and inexpensive devices, there is a huge advantage in using the same software architecture on the cloud, gateways and edge devices.

One of the more common scenarios where an edge device cannot be based on Felxicore is when the edge device needs to be battery driven and battery life needs to be long. In such case, battery powered devices connected to a local Flexicore gatwwaye can be used. A plugin implemented this connection is required.

FlexiCoreServer is an entity that models a remote or local FlexiCore Server, this object will be used to determine the designation of IoT messages.

Configuring local FlexiCore Server

it is expected that the property  flexicore.iot.id will be configured in FlexiCore's configuration file, it is also expected that a JSON describing the remote FlexiCore server will be set in  flexicore.iot.remoteConfigLocation , here is an example of such file:

{
"name":"cloud-test",
"description":"cloud-test",
"enabled":true,
"externalId":"cloud-test",
"basePathApi":"http://localhost:8080/FlexiCore/rest",
"webSocketPath":"ws://localhost:8080/FlexiCore/iotWSGZIP",
"username":"admin@flexicore.com",
"password":"admin"
}

if flexicore.iot.id matches the  externalId field in the JSON it means that a FlexiCoreServer the instance should be created upon startup that describes this (local) server.

Managing FlexiCore Servers

FlexiCore allows creating updating and listing FlexiCore servers using the following endpoints:

  1. /FlexiCore/rest/flexicoreServer/createFlexiCoreServer - creates a  FlexiCoreServer
  2. /FlexiCore/rest/flexicoreServer/registerFlexiCoreServer - when an unknown FlexiCoreServer attempts connecting to our FlexiCoreServer an instance of UnRegisteredFlexiCoreServer will be created and the connection will be closed. this api allows registering one of those UnRegisteredFlexiCoreServer as a proper FlexiCoreServer.
  3. /FlexiCore/rest/flexicoreServer/updateFlexiCoreServer - updates a FlexiCoreServer
  4. /FlexiCore/rest/flexicoreServer/getFlexiCoreServers - lists FlexiCoreServers

or using the following service methods

  1. FlexiCoreServerService.createFlexiCoreServer - creates a  FlexiCoreServer
  2. FlexiCoreServerService.registerFlexiCoreServer - when an unknown FlexiCoreServer attempts connecting to our FlexiCoreServer an instance of UnRegisteredFlexiCoreServer will be created and the connection will be closed. this api allows registering one of those UnRegisteredFlexiCoreServer as a proper FlexiCoreServer.
  3. FlexiCoreServerService.updateFlexiCoreServer - updates a FlexiCoreServer
  4. FlexiCoreServerService.getFlexiCoreServers - lists FlexiCoreServers

FlexiCore Servers Health

remote FlexiCore servers send health information every interval (interval defined on the FlexiCoreServer instance),

the latest health check is held on the FlexiCoreServer instance FlexiCoreServer.isHealthy() and  FlexiCoreServer.lastHealthData() .

history of health checks is also available by calling the endpoint  /FlexiCore/rest/healthReport/getAllHealthReport or by calling HealthReportService.getAllHealthReport

Sending IoT Messages

sending  FlexiCoreIOTRequest  or extenders is possible using IOTService.sendRequest(communicationId,flexiCoreExecutionRequest,c,callback) where :

  • communicationId - remote FlexiCoreServer externalId
  • flexiCoreExecutionRequest - request to send
  • c - type for the expected callback
  •  callback  - callback that will be called once a response for the sent request is received

Receiving IoT Messages

services may register to receive FlexiCoreIOTRequest  of certain types by using  IOTService.registerMessageListener(c,priority,callback) where:

  • c - type for the expected callback
  • priority  - callbacks for the same type will be called by priority order (low is first high is last)
  • callback  - callback that will be called once the request of the given type is received

Existing IoT Messages

  • HealthUpdateRequest will be sent with health data
  • HealthUpdateResponse will be sent once health data was received
  • FlexiCoreServerConnected  will be sent when a FlexiCore server is trying to connect to remote FlexiCore server
  • FlexicoreServerConnectedResponse will be sent with remote server response detonating if it was connected successfully or not
  • FlexiCoreServerDisconnected if possible a FlexiCoreServer will send this message when it disconnects

 

Custom IoT Message Types

developers may extend FlexiCoreIOTRequest  and  FlexiCoreIOTResponse to send custom IOT requests and responses

5.7.2.Synchronization

Synchronization Service allows syncing entities between FlexiCore servers.

this section refers to the following software components:

  1. flexicore-sync-service
  2. flexicore-sync-model

Synchronization relies on IOT.

Instances of entities are synchronized among Flexicore servers if the instance was marked to be synchronized, this carried out by the service creating the instance.

Marking an  Baseclass Entity to be Synced with remote FlexiCore Server

developers need to create a  FlexiCoreServerToBaseclass  using the endpoint:

  •  FlexiCore/rest/plugins/flexiCoreServerToBaseclass/createFlexiCoreServerToBaseclass - providing the  FlexiCoreServer's id and the entity to be synced id

or using the service

  •  FlexiCoreServerToBaseclassService.createFlexiCoreServerToBaseclass - providing the  FlexiCoreServer and the entity to be synced

once this link is created the given entity will be synced to remote FlexiCore Server if it is ever updated

Marking an  BaseclassNoSQL Entity to be Synced with remote FlexiCore Server

developers need to create a  NoSQLSyncLink  using the endpoint:

  •  FlexiCore/rest/plugins/noSQLSyncLink/createNoSQLSyncLink - providing the  FlexiCoreServer's id and the entity to be synced id

or using the service

  •  NoSQLSyncLinkService.createNoSQLSyncLink - providing the  FlexiCoreServer and the entity to be synced

once this link is created the given entity will be synced once and the link will be later deleted

Registering callbacks when an Entity is received

  1. developers may register a callback that will be called once entity of a specific type is received by the syncing service SyncService.registerReceiveType(c,callback) where:
    • c - the class of the entity that the callback will be called for
    • callback - the callback the will be called once the entity is received
  2. developers might require to compare the received entity state with the existing state of the entity:
    •   a converter should first be registered to hold the state of the entity before it was received by calling SyncService.registerConverter(c,syncRegister) where:
      • c - the class of the entity that the converter will receive
      • syncRegister - function receiving the entity and returning an object describing its state, it can be any object
    • a callback receiving the converted object and the received entity should be registered by calling registerCompareCallback(c,postMergeCallback) where:
      • c - the class of the entity that the converter will receive
      • postMergeCallback - function receiving the received entity and the contained entity describing the state before the sync returning null

Versioning Synced Entities

sometimes we might want to update the model in the local server and not in the remote server (or vice-versa), in this case introducing a new non-basic (annotated with JPA's @ManyToOne or @OneToOne) field will break the integrity of the deserialization on the receiving end. FlexiCore allows the developer to solve this problem by using the  @SyncOption  version field, the developer should annotate the class with @SyncOption(version=X) where X is the current version of the entity and should annotate the getter of the non-basic field with @SyncOption(version=Y) where Y is the version the complex field was added in, in this case, FlexiCore will sync the entity setting null value in that field for the remote FlexiCoreServer , once the remote FlexiCoreServer is update to version >=Y this entity will be synced again if it was no updated locally in the remote FlexiCoreServer. here is an example of a versioned entity:

package com.flexicore.example.library.model;

import com.flexicore.annotations.sync.SyncOption;
import com.flexicore.model.Baseclass;
import com.flexicore.security.SecurityContext;

import javax.persistence.Entity;
import javax.persistence.ManyToOne;

@Entity
@SyncOption(version = 4)
public class Book extends Baseclass {

    public Book() {
    }

    public Book(String name, SecurityContext securityContext) {
        super(name, securityContext);
    }

    @ManyToOne(targetEntity = Author.class)
    private Author author;

    @SyncOption(version = 3)
    @ManyToOne(targetEntity = Author.class)
    public Author getAuthor() {
        return author;
    }

    public <T extends Book> T setAuthor(Author author) {
        this.author = author;
        return (T) this;
    }
}

this means that the current version of  Book  entity is 4 and  author  field was added in version 3.

5.7.3.Software Updater

The software update plugin supports the updating and the installation of new components on a remote flexicore server.

In general, the required instances needed on a remote node are transferred through the Flexicore synchronization plugin.

this section refers to the following software component:

  1. software-update-service
  2. software-update-model
  3. command-executor

the software update mechanism relies on Multi-node synchronization and IOT

Software Update Service And Model

Software update service manages  SoftwareUpdate,SoftwareUpdateBundle,SoftwareUpdateInstallation :

  1. SoftwareUpdate - an update of the following types:
    • Service - updates a flexicore service plugin - the received file is a service jar
    • Model - updates a flexicore entities plugin - the received file is an entities jar
    • Core - updates flexicore - the received file is a FlexiCore jar
    • Generic - generic updates - the received file is a zip that contains install.sh script, the script runs from the root of the extracted zip folder.
  2. SoftwareUpdateBundle - a package of multiple SoftwareUpdate
  3. SoftwareUpdateInstallation - an installation request of a SoftwareUpdateBundle on a FlexiCoreServer ( for more on FlexiCoreServer see Multi-node synchronization and IOT) , it contains the current status of installation and any output received while running the installation

installing software on remote FlexiCore server sequence:

  1. if a software installation bundle exists skip to phase 5
  2. create all the relevant SoftwareUpdates objects using the endpoint  /FlexiCore/rest/plugins/SoftwareUpdate/createSoftwareUpdate
  3. create a SoftwareUpdateBundle  by calling   /FlexiCore/rest/plugins/SoftwareUpdateBundle/createSoftwareUpdateBundle
  4. for each update from section 2 connect the bundle with that update by calling /FlexiCore/rest/plugins/UpdateToBundle/createUpdateToBundle
  5. create SoftwareUpdateInstallation by calling /FlexiCore/rest/plugins/SoftwareUpdateInstallation/createSoftwareUpdateInstallation

 

Command Executor

For FlexiCore to run the updates in a reliable manner it must have the command executor run as a service, it is in charge of restarting services, updating flexicore, and generally running all the commands received from the local flexicore server.

Command executor listens by default on the loopback interface , it is configurable but it is recommended not to set it to listen to public interfaces for security reasons.

Installing Command Executor on Ubuntu

assuming the command-executor-*.jar is at  /home/flexicore/command-executor-*.jar  here is the required systemd service:

[Unit]
Description=Command Executor
After=syslog.target network.target
Before=httpd.service

[Service]
User=root
LimitNOFILE=102642
PIDFile=/var/run/commandExecutor/commandExecutor.pid
ExecStart=/usr/bin/java -jar /home/flexicore/command-executor-1.0.0.jar --logging.file=/var/log/commandExecutor/command-executor.log
StandardOutput=null

[Install]
WantedBy=multi-user.target

 

Note: The Command executor runs as root user , this is required for privileged actions such as machine/service restart and manipulating files in privileged locations.

 

5.7.4.Rules Engine

5.7.5.Organization management

The Organization Service supports managing organization objects: Branch,Customer,Employee,Industry,Organization,SalesPerson,SalesRegion,Site,Supplier

this section refers to the following software components:

  1. organization-service
  2. organization-model

Organization Service depends on Address Management component.

 

Managing Organization Entities

managing organization objects is possible using the following endpoints:

  1. /FlexiCore/rest/plugins/Branch/getAllBranches - getting all branches
  2. /FlexiCore/rest/plugins/Branch/createBranch - create branch
  3. /FlexiCore/rest/plugins/Branch/updateBranch - update branch
  4. /FlexiCore/rest/plugins/Customer/getAllCustomers - getting all customers
  5. /FlexiCore/rest/plugins/Customer/createCutomer - create customer
  6. /FlexiCore/rest/plugins/Customer/updateCustomer - update customer
  7. /FlexiCore/rest/plugins/Employee/listAllEmployees - getting all employees
  8. /FlexiCore/rest/plugins/Employee/createEmployee - create employee
  9. /FlexiCore/rest/plugins/Employee/updateEmployee - update employee
  10. /FlexiCore/rest/plugins/Industry/getAllIndustries - getting all industries
  11. /FlexiCore/rest/plugins/Industry/createIndustry - create industry
  12. /FlexiCore/rest/plugins/Industry/updateIndustry - update industry
  13. /FlexiCore/rest/plugins/SalesPerson/listAllSalesPersons - get all salespersons
  14. /FlexiCore/rest/plugins/SalesPerson/createSalesPerson - create a salesperson
  15. /FlexiCore/rest/plugins/SalesPerson/updateSalesPerson - updates salesperson
  16. /FlexiCore/rest/plugins/SalesRegion/listAllSalesRegions - getting all sales regions
  17. /FlexiCore/rest/plugins/SalesRegion/createSalesRegion - create a sales region
  18. /FlexiCore/rest/plugins/SalesRegion/updateSalesRegion - update a sales region
  19. /FlexiCore/rest/plugins/Site/getAllSites - getting all sites
  20. /FlexiCore/rest/plugins/Site/createSite - create site
  21. /FlexiCore/rest/plugins/Site/updateSite - update site
  22. /FlexiCore/rest/plugins/Supplier/getAllSuppliers - getting all suppliers
  23. /FlexiCore/rest/plugins/Supplier/createSupplier - create supplier
  24. /FlexiCore/rest/plugins/Supplier/updateSupplier - update supplier

or by using the matching services:

  1. BranchService.getAllBranches - getting all branches
  2. BranchService.createBranch - create branch
  3. BranchService.updateBranch - update branch
  4. CustomerService.getAllCustomers - getting all customers
  5. CustomerService.createCutomer - create customer
  6. CustomerService.updateCustomer - update customer
  7. EmployeeService.listAllEmployees - getting all employees
  8. EmployeeService.createEmployee - create employee
  9. EmployeeService.updateEmployee - update employee
  10. IndustryService.getAllIndustries - getting all industries
  11. IndustryService.createIndustry - create industry
  12. IndustryService.updateIndustry - update industry
  13. SalesPersonService.listAllSalesPersons - getting all salespersons
  14. SalesPersonService.createSalesPerson - creating salesperson
  15. SalesPersonService.updateSalesPerson - updates salesperson
  16. SalesRegionService.listAllSalesRegions - getting all sales region
  17. SalesRegionService.createSalesRegion - create sales region
  18. SalesRegionService.updateSalesRegion - update sales region
  19. SiteService.getAllSites - getting all sites
  20. SiteService.createSite - create site
  21. SiteService.updateSite - update site
  22. SupplierService.getAllSuppliers - getting all suppliers
  23. SupplierService.createSupplier - create supplier
  24. Supplier.updateSupplier - update supplier

Class Diagram

Organization Service Class Diagram
Organization Service Class Diagram

5.7.6.Address Management

The Address Service supports managing address related objects:

 Address,Country,City,Neighbourhood,State,Street

this section refers to the following software components:

  1. flexicore-territories-service
  2. flexicore-territories-model

Managing Address Objects

the following endpoints are used to list/create/update the address objects:

  1.  /FlexiCore/rest/plugins/Address/listAllAddresses  - lists addresses
  2.  /FlexiCore/rest/plugins/Address/createAddress  - creates address
  3.  /FlexiCore/rest/plugins/Address/updateAddress  - updates address
  4.  /FlexiCore/rest/plugins/City/listAllCities  - lists cities
  5.  /FlexiCore/rest/plugins/City/createCity  - create city
  6.  /FlexiCore/rest/plugins/City/updateCity - update city
  7.  /FlexiCore/rest/plugins/Country/listAllCountries  - lists countries
  8.  /FlexiCore/rest/plugins/Country/createContry  - create country
  9.  /FlexiCore/rest/plugins/Country/updateCountry  - update country
  10.  /FlexiCore/rest/plugins/Neighbourhood/listAllNeighbourhoods  - lists neighborhoods
  11.  /FlexiCore/rest/plugins/Neighbourhood/createNeighbourhood  - creates neighborhood
  12.  /FlexiCore/rest/plugins/Neighbourhood/updateNeighbourhood  - updates neighborhood
  13.  /FlexiCore/rest/plugins/State/listAllStates  - lists states
  14.  /FlexiCore/rest/plugins/State/createState  - creates state
  15.  /FlexiCore/rest/plugins/State/updateState  - updates state
  16.  /FlexiCore/rest/plugins/Street/listAllStreets  - lists streets
  17.  /FlexiCore/rest/plugins/Street/createStreet  - creates street
  18.  /FlexiCore/rest/plugins/Street/updateStreet  - updates street

or the following services:

  1.  AddressService.listAllAddresses  - lists addresses
  2.  AddressService.createAddress  - creates address
  3.  AddressService.updateAddress  - updates address
  4.  CityService.listAllCities  - lists cities
  5.  CityService.createCity  - create city
  6.  CityService.updateCity - update city
  7.  CountryService.listAllCountries  - lists countries
  8.  CountryService.createContry  - create country
  9.  CountryService.updateCountry  - update country
  10.  NeighbourhoodService.listAllNeighbourhoods  - lists neighborhoods
  11.  NeighbourhoodService.createNeighbourhood  - creates neighborhood
  12.  NeighbourhoodService.updateNeighbourhood  - updates neighborhood
  13.  StateService.listAllStates  - lists states
  14.  StateService.createState  - creates state
  15.  StateService.updateState  - updates state
  16.  StreetService.listAllStreets  - lists streets
  17.  StreetService.createStreet  - creates street
  18.  StreetService.updateStreet  - updates street

Class Diagram

Address Class Diagram
Address Class Diagram

 

5.7.7.Billing management

The Billing Service supports managing billing-related objects:  Contract,BusinessService,ContractItem,Currency,Invoice,InvoiceItem,
PaymentMethod,PaymentMethodType,PriceList,PriceListToService
.

this section refers to the following software components:

  1. billing-service
  2. billing-model

Billing Service depends on Address Management and Organization management.

the service contains CRUD methods and REST endpoints for managing the mentioned objects above and does not contain any actual defrayment method.

Adding Payment Methods

developers are expected to create a model and a service plugins for the new payment method and:

  1. create a  PaymentMethodType on startup (make it is created only once) with the relevant name and description.
  2. extend  PaymentMethod the entity with more specific properties required to implement the interfacing with the payment service.
  3. add REST APIs or Dynamic invokers to create the  PaymentMethod extender defined in phase 2.
  4. implement  PaymentMethodService pay method.

Using the other base objects such as Contract,BusinessService,ContractItem the billing service will charge the customer at the required times.

Billing Service Class Diagram
Billing Service Class Diagram

5.7.8.Equipment and Products

The product (and equipment) plugin supports the management Equipment,Product,ExternalServer,ProdcutStatus .

These services and models were designed mainly for an IoT system but necessarily so.

This system can be used, for example,  to track equipment inventory. If model or services lack functionality, entity inheritance and new services injected with the existing ones may be used to extend the system herewith as required. This is the spirit of Flexicore.

this section refers to the following software component:

  1. product-service
  2. product-model

product service depends on Organization management and Address Management.

Concepts

Equipment may have an external server that communicates with the equipment, in such case such server may be connected to Flexicore based server via the external server API. Such support is normally implemented through another plugin.

Alternatively, a local Flexicore server (can be regarded as a Gateway) may be connected directly to instances of equipment and synchronize its data and state with a 'main' Flexicore based server.

The product service supports the inspection of an external server for updates for the already existing equipment instances, this is used if the source of the status of this equipment is that external server, product service will automatically update the connectivity status of equipment instance by changing their status to "Connected" or "Disconnected" respectively.

The instances of the equipment may be periodically imported from the external server online or through some other import method. This is usually done as a method in an invoker or an explicit API. An explicit API cannot be called from a schedule instance. The invoker method is always preferred.

Managing Equipment

the product service supports CRUD methods for equipment, product, external server, and product status.

Other Common Services

Equipment is an entity which optionally has latitude and longitude, some of the common services product-service supports are:

  1.  plugins/Equipments/getAllEquipmentsGrouped - returns equipment grouped by Geohash precision.
  2.  plugins/Equipments/getProductGroupedByStatusAndType - returns equipment grouped by  ProductStatus and  ProductType .

Adding New External Server Type Support

adding support for new external server type is done by implementing service and model plugins and:

  1. extending  ExternalServer adding specific properties required for starting connection.
  2. extending  ConnectionHolder adding specific properties required to maintaining the connection.
  3. registering an external server connection by firing the event  ExternalServerConnectionConfiguration implementing the following methods:
    •  onConnect- called per  ExternalServer of the extended type (in phase 1) returns the extended ConnectionHolder from phase 2.
    •  onRefresh- called to fetch extenders of ExternalServer (from phase 1)
    •  onInspect- called per  ExternalServer of the extended type (in phase 1) returns the extended ConnectionHolder from phase 2.
    •  onInspect- called per  ExternalServer of the extended type (in phase 1) returns the extended ConnectionHolder from phase 2.
    •  onSingleInspect (optional) - will be called when  SingleInspectJob (which is intended to inspect only a single Equipment) is fired.

 

Product Service Class Diagram
Product Service Class Diagram

5.7.9.Dynamic User Interface

FlexiCore allows developers to create a dynamic UI in a variety of ways, the main goals of dynamic UI are:

  1. reducing the necessity of changing the UI client-side code when server-side features are implemented
  2. eliminate the need to support multiple versions of the UI for multiple clients.

5.7.9.1.UIComponents

FlexiCore's  UIComponent leverages FlexiCore's out of the box state of the art permission system and access control and allows the UI to register objects reflecting some of the UI elements on the screen or some front-end property or action, then an administrator can allow/deny this UIComponent for certain users, roles or tenants via the Access Control Management System. the UI client-side code may change the UI behavior(for example hiding it ) if the UIComponent is denied for the user.

When UI components are not used, and all users see the same user interface, operations blocked by the server will fail with a server-side error  (Forbidden 403), UIcomponents allow a cleaner behavior where some users should be blocked from accessing an operation.

 

 

API and workflow

using REST endpoint:

/FlexiCore/rest/uiPlugin/registerAndGetAllowedUIComponents - receives a body that contains a list of UI components for each:

UI components that do not exist ( identified by externalId property) are created, the response of the API will be a list of allowed UI components which is a subset of the UI components provided to the API.

The interpretation of what to do when a UI component is available or not is solely a client-side code decision, common use cases are:

  • Make a widget invisible/visible.
  • Make a widget enabled/disabled (if both visible/invisible and enabled/disabled are required, create two UIComponents).
  • Create a completely different panel or dialog for that user.

or by using the service:

UIComponentService.registerAndGetAllowedUIComponents - receives a body that contains a list of ui components for each:

UI components that do not exist ( identified by externalId property)  are created, the response of the API will be a list of allowed UI components which is a subset of the UI components provided to the API.

the usage flow of UIComponent feature is usually as follows:

  1. The client-side designer identifies components he would like to hide/show based on the logged-in user.
  2. The client-side designer assigns externalID , name, and description for each component from phase 1.
  3. The client-side code calls  /FlexiCore/rest/uiPlugin/registerAndGetAllowedUIComponents which returns the UIComponent the logged-in user is allowed to access.
  4. The client-side shows/hides relevant UI based on phase 3. However, the interprtaion of the existence of the UIComponent in the returned response for the current user is totally up to the client code.

5.7.9.2.UI Presets

the ui preset feature is used to describe a set of frequently used ui components allowing the a managing user to define and customize them.

a managing user is NOT a developer , this allows the system to expose features without any development requirement ( once the initial support for ui preset is done)

Note that ui preset feature highly relies on Dynamic invokers.

the plugins used by the ui preset feature are:

  1. flexicore-ui-service
  2. flexicore-ui-model

setting the priority of preset is a common service for all preset types (listed below) and it possible by using the following endpoints:

  1.  /FlexiCore/rest/plugins/UiFields/linkPresetToUser - links preset with user providing:
    •  presetId - the id of the preset to set the link for.
    •  userId - the user id to set the link for.
    •  priority - priority of the preset for this user , lower value indicates higher priority.
    •  enabled - if this preset is enabled for this user.
  2. /FlexiCore/rest/plugins/UiFields/linkPresetToRole - links preset with user providing:
    •  presetId - the id of the preset to set the link for.
    •  roleId - the role id to set the link for.
    •  priority - priority of the preset for this role , lower value indicates higher priority.
    •  enabled - if this preset is enabled for this role .
  3. /FlexiCore/rest/plugins/UiFields/linkPresetToTenant - links preset with user providing:
    •  presetId - the id of the preset to set the link for.
    •  tenantId - the tenant id to set the link for.
    •  priority - priority of the preset for this tenant, lower value indicates higher priority.
    •  enabled - if this preset is enabled for this tenant.

 

5.7.9.2.1.Grid preset

GridPreset  is an entity that includes a  DynamicExecution  and UI configuration string.

GridPreset  and the services supporting it are allowing a managing user to create grids for dynamic data (the data source must support invokers as defined in Dynamic invokers Section).

The grid preset supports column management, run time filtering and sorting as well as CSV export of the filtered grid data.

Creating a Grid

  1. client-side code should create a  DynamicExecution as described in Dynamic invokers section.
  2. client-side code should create a GridPreset  entity by calling  /FlexiCore/rest/plugins/GridPresets/createGridPreset providing the created dynamic execution from phase 1.

Customizing the Grid

Once a grid is created it is possible to select the visible fields in the grid of the returned object by creating the  TableColumn entity with the following properties:

  • name - must be the nested JSON path (dot separated) inside the returned object.
  • sortable - if the field is sortable or not at runtime (when the grid is displayed).
  • filterable - if the field is filterable.
  • defaultColumnWidth - default column width when opening the presenting the preset. This is better provided in percents, the total width of all columns can exceed 100% as a horizontal scroll bar can be provided by the client-side.
  • presetId - the id of the preset this TableColumn is connected to.
  • priority - used to determine the order of the columns.
  • visible - if the column is visible or not
  • dynamicField - if the column described a schema specified property or Dynamic Property.
  • categoryName - the name of the category, this allows client-side to group columns by category.
  • display name - name to display for the column.

 

 

5.7.9.2.2.Tree Preset

 

 

5.7.9.2.3.Dashboard Preset

5.7.9.3.UI Plugins

5.7.10.Localization

The Localization Service supports translating server-side and client-side objects.

this section refers to the following software components:

  1. translation-service
  2. translation-model

Concepts

Localization Service revolves around the  Translation object which has an  externalId field or  translated  field which is of type Baseclass and a  languageCode  ( this is determined by the creator of the translation object but usually it should be ISO 639-1 ). for translating server-side objects the client should provide the  Baseclass ids to translate , to translate client-side objects the client should provide externalId which is a unique id referring to the client-side object to be translated.

using plain i18n to describe server side objects is unrealistic as it means the i18n file has to include all of the described entities which makes it harder to manage and maintain.

Example Workflow

assuming we have a list of countries with a field called name which contains the name of the country and the values are in English and we would like to present them in Spanish , the client side workflow is as follows:

  1. fetch country entities as you normally would
  2. load i18n "file" from the url  /FlexiCore/rest/plugins/Translations/generateI18nJsonAll/{langCode} providing "es" as langCode

Managing Translation objects

creating updating or listing translation objects is possible using the following endpoints:

  1.  /FlexiCore/rest/plugins/Translations/getAllTranslations - getting the translation objects
  2.  /FlexiCore/rest/plugins/Translations/updateTranslation - updating translation objects
  3.  /FlexiCore/rest/plugins/Translations/createTranslation - creating translation  object

or using the following  TranslationService methods

  1.  getAllTranslations - getting the translation objects
  2.  updateTranslation - updating translation objects
  3.  createTranslation - creating translation  object

 

I18n Support

the translation service supports importing/exporting from/to i18n format using the following endpoints:

  1.  /FlexiCore/rest/plugins/Translations/generateI18nJson - accepts a filter body and returns a json in i18n format
  2.  /FlexiCore/rest/plugins/Translations/generateI18nJsonAll/{langCode} - receives  the required langCode as parameter , this api is usfull when client requires a GET HTTP request
  3.  /FlexiCore/rest/plugins/Translations/importI18n - imports translation objects from i18n files

or via the following service methods:

  1.  generateI18nJson - accepts a filter and returns a json in i18n format
  2.  importI18n - imports translation objects from i18n files

5.7.11.Licensing

FlexiCore allows plugin developers to restrict features of the system only to allowed customers. the license is valid only for the given computer using network interfaces MAC address, external hardware id, or disk serial number.

Licenses are provided per tenant, thus, a business-to-business system can provide additional paid-for features to some of its customers.

Configuring Licensed Components

developer should annotate its REST(or WebSocket or invoker) class with  @OperationsInside  and specify the  features property for the features that are required to be licensed by the calling user, for each feature provide:

  • canonicalName - this is the feature's unique identifier
  • productCanonicalName - this is the feature's related product canonical name

note that each product consists of one or more features so a user may have a license for a specific feature or for the whole product.

a feature must be annotated with  @ProtectedREST to be enforced.

Configuring Licensed Entities

Sometimes quantity based license is required (for example limiting created users to 50), FlexiCore allows enforcing by specifying the limitation when signing/requesting a license request.

Listing Licensing Products and Features

FlexiCore exposes the available license features and products using the following endpoints:

  1.  /FlexiCore/rest/licensingProducts/getAllLicensingProducts - lists licensing products
  2. /FlexiCore/rest/licensingFeatures/getAllLicensingFeatures - lists licensing features

Requesting a License

The user should create a  LicenseRequest  object and attach the requested features and products by creating  LicenseRequestToFeature,LicenseRequestToQuantityFeature and  LicenseRequestToProduct  using the following endpoints:

  1. /FlexiCore/rest/licenseRequests/createLicenseRequest - creates a license request
  2. /FlexiCore/rest/licenseRequestToFeatures/createLicenseRequestToFeature - attaches a license feature to a license request
  3. /FlexiCore/rest/licenseRequestToProducts/createLicenseRequestToProduct - attaches a license product to a license request
  4. /FlexiCore/rest/licenseRequestToQuantityFeatures/createLicenseRequestToQuantityFeature - attaches a quantity license feature to a license request

Then the user should download the  requestFile  in the LicenseRequest  (see File management support on how to download files) and provide it to a signing user (this is carried out on a different system in most cases).

Signing a License request

using the license request signer tool the signing user should do as follows to sign a license request:

  1. create public/private keys or load existing ones
  2. load the request to sign - this is sent by the requesting user
  3. add quantity based licensing features
  4. sign the request
  5. send the certificate and the public key to the requester

Importing signed request

once the license granter approved the license request the license requester should:

  1. update the public key file (see Installing FlexiCore )
  2. update the license request creating LicenseRequestToQuantityFeature if this was added to the updated request file
  3. upload the certificate file (see File management support on how to upload files)
  4. update the license request by calling /FlexiCore/rest/licenseRequests/updateLicenseRequest providing the certificate  FileResource id

 

 

 

 

 

5.7.12.Scheduling

the Scheduling component allows managing Scheduling objects, their time slots, and actions.

Scheduling component is implemented by the following software components:

  1. scheduling-model
  2. scheduling-service

Scheduling actions greatly relies on Dynamic invokers.

Concepts

Scheduling is used to time one or more actions (for example sending mail or fetching data from remote server) in one or more time slots.

selecting an action is possible from the already implemented dynamic invokers or by adding a new dynamic invoker.

Managing Scheduling Entities

create update and list  Schedule entity using the following endpoints:

  1. /FlexiCore/rest/plugins/Scheduling/getAllSchedules  - returns a list of schedules
  2. /FlexiCore/rest/plugins/Scheduling/createSchedule - creates a schedule,main properties listed below:
    • name - The name of the schedule
    • Sunday - Saturday - enabled for each day of the week
    • holiday enabled for holidays
    • timeFrameStart - the date the schedule will be valid from
    • timeFrameEnd - the date the schedule will stop be valid from
  3. /FlexiCore/rest/plugins/Scheduling/updateSchedule - updates a schedule

or using the following service methods:

  1. SchedulingService.getAllSchedules  - returns a list of schedules
  2. SchedulingService.createSchedule - creates a schedule
  3. SchedulingService.updateSchedule - updates a schedule

create update and list  ScheduleTimeslot entity using the following endpoints:

  1. /FlexiCore/rest/plugins/Scheduling/getAllScheduleTimeslots  -returns a list of schedule time slot
  2. /FlexiCore/rest/plugins/Scheduling/createScheduleTimeslot - creates a schedule time slot
  3. /FlexiCore/rest/plugins/Scheduling/updateSchedule -updates a schedule time slot

or using the following service methods:

  1. SchedulingService.getAllScheduleTimeslots  - returns a list of schedule time slot
  2. SchedulingService.createScheduleTimeslot - creates a schedule time slot
  3. SchedulingService.updateScheduleTimeSlot - updates a schedule time slot

create update and list  ScheduleAction entity using the following endpoints:

  1. /FlexiCore/rest/plugins/Scheduling/getAllScheduleActions  - returns a list of schedule Actions
  2. /FlexiCore/rest/plugins/Scheduling/createScheduleAction - creates a schedule Action
  3. /FlexiCore/rest/plugins/Scheduling/updateScheduleAction - updates a schedule Action

or using the following service methods:

  1. SchedulingService.getAllScheduleActions  - returns a list of schedule actions
  2. SchedulingService.createScheduleAction - creates a schedule , providing:
    • dynamicExecutionId- the id of the dynamic execution to use for this action
  3. SchedulingService.updateScheduleAction - updates a schedule action

linking/unlinking  SchedulingAction  and  Scheduleusing the following endpoint:

  1. /FlexiCore/rest/plugins/Scheduling/linkScheduleToAction  - links an action with a schedule
  2. /FlexiCore/rest/plugins/Scheduling/unlinkScheduleToAction- unlinks an action with a schedule

or using the following service methods:

  1. SchedulingService.linkScheduleToAction  -links action with a schedule
  2. SchedulingService.unlinkScheduleToAction - unlinks an action with a schedule

Class Diagram

Scheduling Class Diagram
Scheduling Class Diagram

 

 

 

 

 

`

 

5.7.13.Order Management

The order service plugin supports managing Orders.

The main use case here is tracking goods ordered from a supplier. Orders can be linked to a consuming organization, that is, the customer for this order.

this section refers to the following software components:

  1. order-service
  2. order-model

order service depends on Organization managementAddress Management and Equipment and Products .

Managing Orders

order service manages  Order,OrderItem,SupplyTime  entities.

Sending Order to Supplier

Sometimes it is required to send orders to suppliers for processing, this is done by creating a service and model plugins and follow the steps:

  1. create a supplier entity representing the supplier you connect to.
  2. when creating the order set the supplier to the one created in phase 1.
  3. implement a logic that will send the order and implement (for example Scheduling)

 

Order Service Class Diagram
Order Service Class Diagram

5.8.Front End SDK

5.9.Swagger

FlexiCore has out of the box support for Swagger .

FlexiCore protects the swagger UI and only authorized users are allowed to access it , the swagger interface is available at  /FlexiCore/index.html

 

5.10.Deploying Flexicore Applications and Systems

5.11.Build Tools plugins

FlexiCore provides a maven plugin to fix issues with meta-model generation between entity plugins.

<plugin>
    <groupId>com.wizzdi</groupId>
    <artifactId>metamodel-inheritence-fix-processor</artifactId>
    <version>1.0.1</version>
    <executions>
        <execution>
            <id>flexicore</id>
            <phase>process-sources</phase>
            <goals>
                <goal>process</goal>
            </goals>
        </execution>
    </executions>
</plugin>

 

6.How to Guides

7.Using Flexicore In Micro-services

Suggest Edit