Skip to content

Quak Core

John Ernest Amiscaray edited this page Jan 22, 2025 · 9 revisions

The Quak Core module contains core functionalities for a quak application to function. This includes application lifecycle hooks, application configuration, and dependency injection.

Application Lifecycle Hooks

Quak represents an application using the Application class. This class includes functionality for application lifecycle hooks. Different phases of the application lifecycle are represented using the LifecycleState enum defined in the Application class. Callbacks can be added for each of these lifecycle states using the Application#on method. The following are the application lifecycle states and their meanings:

  • PRE_START : Before the application starts.
  • CONTEXT_LOADED : After the application context loads (see the section below on dependency injection).
  • POST_START : After the application starts.
  • PRE_STOP : Before the application stops.
  • POST_STOP : After the application stops.

Note that if you are starting up a WebApplication using the WebStarter class, you should use the WebStarter#on method to add the application lifecycle hooks. This is supposed to be called before the WebStarter#beginWebApplication method.

Dependency Injection

Quak framework has a robust API for dependency injection. Quak implements dependency injection using a combination of constructor-based dependency injection, annotation-based dependency providers, JPMS service loading, and an application singleton holding the dependencies. Using Quak's dependency injection, you can define dependencies using any of the following strategies:

JPMS Service Loading Strategy Example

package io.john.amiscaray.quak.security.di;

import io.john.amiscaray.quak.core.di.ApplicationContext;
import io.john.amiscaray.quak.core.di.dependency.DependencyID;
import io.john.amiscaray.quak.core.di.dependency.ProvidedDependency;
import io.john.amiscaray.quak.core.di.provider.DependencyProvider;
import io.john.amiscaray.quak.security.cors.filter.CORSFilter;

import java.util.List;

/**
 * Provides the application with a CORS filter.
 */
public class CORSFilterProvider implements DependencyProvider<CORSFilter> {

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

    @Override
    public DependencyID<CORSFilter> getDependencyID() {
        return new DependencyID<>(SecurityDependencyIDs.CORS_FILTER_DEPENDENCY_NAME, CORSFilter.class);
    }

    @Override
    public ProvidedDependency<CORSFilter> provideDependency(ApplicationContext context) {
        return new ProvidedDependency<>(getDependencyID(), new CORSFilter(context.getInstance(SecurityDependencyIDs.SECURITY_CONFIG_DEPENDENCY)));
    }

    @Override
    public List<DependencyID<?>> getDependencies() {
        return List.of(SecurityDependencyIDs.SECURITY_CONFIG_DEPENDENCY);
    }
}

along with a module-info declaration:

module my.module {
    provides DependencyProvider with CORSFilterProvider;
}

ManagedType Annotation Example

package io.john.amiscaray.test.security;

import io.john.amiscaray.quak.core.di.provider.annotation.ManagedType;
import io.john.amiscaray.quak.security.auth.Authenticator;
import io.john.amiscaray.quak.security.auth.credentials.Credentials;
import io.john.amiscaray.quak.security.auth.principal.Principal;
import io.john.amiscaray.quak.security.auth.principal.RoleAttachedPrincipal;
import io.john.amiscaray.quak.security.auth.principal.role.Role;
import io.john.amiscaray.quak.security.di.SecurityDependencyIDs;
import io.john.amiscaray.test.security.roles.Roles;

import java.time.Duration;
import java.util.Optional;

@ManagedType(dependencyName = SecurityDependencyIDs.AUTHENTICATOR_DEPENDENCY_NAME, dependencyType = Authenticator.class)
public class SimpleAuthenticator implements Authenticator {

   // Some code goes here...

}

Provider Example

package io.john.amiscaray.test.security.di;


import io.john.amiscaray.quak.core.di.provider.annotation.Instantiate;
import io.john.amiscaray.quak.core.di.provider.annotation.Provide;
import io.john.amiscaray.quak.core.di.provider.annotation.ProvidedWith;
import io.john.amiscaray.quak.core.di.provider.annotation.Provider;
import io.john.amiscaray.quak.security.config.CORSConfig;
import io.john.amiscaray.quak.security.config.EndpointMapping;
import io.john.amiscaray.quak.security.config.SecurityConfig;
import io.john.amiscaray.quak.security.di.AuthenticationStrategy;
import io.john.amiscaray.quak.security.di.SecurityDependencyIDs;
import io.john.amiscaray.test.security.roles.Roles;

import java.time.Duration;
import java.util.List;

@Provider
public class SecurityConfigProvider {

    private final String jwtSecret;

    @Instantiate
    public SecurityConfigProvider(@ProvidedWith(dependencyName = "jwt") String jwtSecret) {
        this.jwtSecret = jwtSecret;
    }

    @Provide(dependencyName = SecurityDependencyIDs.SECURITY_CONFIG_DEPENDENCY_NAME)
    public SecurityConfig securityConfig() {
        return SecurityConfig.builder()
                .securePathWithRole(new EndpointMapping("/studentdto/*", List.of(EndpointMapping.RequestMethodMatcher.ANY_MODIFYING)), List.of(Roles.admin()))
                .securePathWithCorsConfig("/*", CORSConfig.builder()
                        .allowOrigin("http://127.0.0.1:5500")
                        .allowMethod("GET")
                        .build())
                .authenticationStrategy(AuthenticationStrategy.JWT)
                .jwtSecretKey(jwtSecret)
                .jwtSecretExpiryTime(Duration.ofHours(10).toMillis())
                .build();
    }

}

Dependencies

Dependencies in Quak are internally represented using the ProvidedDependency record. This contains the provided instance and a io.john.amiscaray.quak.core.di.dependency.DependencyID. The dependency ID contains a type for the instance and an optional String identifier in case you need multiple dependencies of the same type.

ApplicationContext

ApplicationContext is an application singleton used to access dependencies. It contains methods for asserting a dependency exists:

ApplicationContext.getInstance().hasInstance(MyClass.class);
// or
ApplicationContext.getInstance().hasInstance(new DependencyID("myDependency", MyClass.class));

or retrieving them by either their type or their dependency ID:

MyClass obj = ApplicationContext.getInstance().getInstance(MyClass.class);
// or
MyClass obj = ApplicationContext.getInstance().getInstance(new DependencyID("myDependency", MyClass.class));

It also has functionality for aggregating dependencies into a list using a @AggregateTo Annotation which can be placed on a @Provide method or an @Instantiate constructor. Using this, we can retrieve the list of dependencies like so:

List<MyClass> instances = ApplicationContext.getInstance().getAggregateDependencies("Dependencies", MyClass.class);

Quak resolves the application context at the start of your application's lifecycle. This means that it goes through all the dependencies and attempts to instantiate all of them. Quak provides an application lifecycle hook for when the application context is loaded called CONTEXT_LOADED. If Quak fails to instantiate any required dependencies, the application will fail.

The DependencyProvider Interface

Dependencies are provided to Quak using the DependencyProvider interface (see the example above). Whether you provide dependencies to the framework using an annotation-based approach or using JPMS service loading, dependency providers will be represented using this interface. This contains methods for retrieving the dependency ID of the dependency we are providing, retrieving the dependency IDs of the dependencies needed to instantiate this dependency, testing whether this dependency is required to start the application, retrieving an identifier of a list to aggregate this dependency to, and retrieving the provided dependency given the application context.

Providing Dependencies Using Service Loading

As seen above, dependencies can be provided to application context using JPMS' service loading functionality. This can be done by implementing the above-mentioned DependencyProvider interface and adding a provides statements in your application's module info.

Note that your application needs a module-info.java file for Quak to provide some required dependencies for you (ex. quak.framework.data's DatabaseProxy). This can be generated for you using the quak.framework.generator maven plugin.

Annotation-based Dependency Providers

Dependency providers can be implemented using an annotation-based approach (see the example above). A dependency provider can be represented by any class annotated with @Provider these classes can contain methods annotated with @Provide saying that the return value will be added to the application context. At the beginning of your application's lifecycle, these providers will be used to implement the DependencyProvider interface, get instantiated and added to the application context, then provide additional dependency using these @Provide methods.

As alluded to earlier, a dependency provider is also added as a dependency to the application context. Thus, we can use constructor-based dependency injection to instantiate the provider using a constructor annotated with @Instantiate. Any parameters must first be instantiated into the application context. Similarly, each @Provide method can accept arguments for instances in the application context.

ManagedTypes

Application components that you can instantiate via constructor-based dependency injection are annotated with @ManagedType (see the example above). Like the annotation-based providers, these can be instantiated via constructor using the @Instantiate annotation. On application startup, these will be added to the application context.

Clone this wiki locally