This document provides a list of rules and best practices followed by project Helidon. Please follow these rules when contributing to the project, when refactoring existing code and when reviewing changes done by others.
Exceptions to these rules should be documented clearly.
- Use unchecked Throwables - descendants of RuntimeException in API
- Never use RuntimeException directly - always create a descendant appropriate for your module, or use an existing exception declared in the module
- Our APIs should never throw a checked exception unless enforced by implemented/extended interface - e.g. when we implement a java.io.Closeable, we must declare the checked exception.
- Usage of
null
is discouraged and should not exist in any public APIs of Helidon- If a method accepts a
null
, refactor it to a different approach- a setter: create a method to remove the field value rather than setting a
null
value (such ashost(String)
to set a host, andunsetHost()
to revert to default value) - other methods:
- if there is a low number of combinations (up to 2), create another method without the parameter
- otherwise create a parameter object that uses a builder to configure optional parameters
- never use
java.util.Optional
as a parameter type
- a setter: create a method to remove the field value rather than setting a
- If a method would return
null
, returnjava.util.Optional
instead
- If a method accepts a
- We use flat package structure
- Each module (maven and jigsaw) has a single implementation package
- Each maven jar packaging module used outside of testing is also a jigsaw module
- Module may have an additional package "spi" for classes related to service provider interface (extensibility)
- Unit testing is enabled through package local access (not public!)
- Be aware that any public class and its public methods are part of Helidon API and will require careful maintenance
- Do not rely on java module system (JPMS/Jigsaw) to enforce visibility
- If a set of classes seems to require a separate package, it is a good candidate for a separate module
- Example: there could be a package for each "abac" module in "abac" security provider. Even though these modules are mostly very small, these were extract to standalone modules, not to break the rule of flat package structure. In general this helps enforce the rule of separation of concerns - if you feel you need a new package, in most cases you are putting together different concerns in a single module.
- Each module (maven and jigsaw) has a single implementation package
- Naming conventions of maven modules, maven module directories and package names are connected:
- Directories: name of the directory is module name (referred to as ${module_name} further in this document)
- For pom packaging, the module is a "project module" (considering modules that serve as aggregators for sub-modules into a common reactor)
- Maven coordinates:
- Group id: io.helidon.${project_module}* - such as io.helidon.webserver; io.helidon.microprofile.config
- Artifact id: helidon-${module_name}(-project)? - such as "helidon-security", "helicon-security-project", where project modules use the suffix "-project"
- Version: always inherited
- Package names:
- io.helidon.${project_module}*.${module_name} - e.g. io.helidon.security, io.helidon.security.providers.common
- Directories: name of the directory is module name (referred to as ${module_name} further in this document)
- Everything that can be done using config, must be possible using programmatic approach through builders
- Exceptions:
- CDI components configured from a CDI Extension (we still have support for explicitly defining the extension in Server Builder)
- Exceptions:
- Everything that can be done using builders should be possible also using configuration, except for cases that would mandate usage of reflection (such an exception may be configuration of Routing in WebServer - nevertheless we still may support it (emphasis on "may" rather than "should"), or configuration of security for Jersey resources)
- When accepting config as a parameter, we should expect the config is located on the node that contains our configuration (such as in ServerConfiguration in WebServer)
- Config keys:
- Use lower case words separated by dashes (e.g. "token-endpoint-uri", NOT "tokenEndpointUri")
- May be nested in a tree structure (e.g. outbound-token.name, outbound-token.algorithm)
- The following properties may be used by a component:
- Required: component will fail to build when such a configuration property is missing
- Default: component has a well defined and documented default value for such a property
- Optional: component behaves in a well defined and documented manner if such a property is not configured (e.g. a component may expect tracing endpoint - if not defined, tracing may be disabled)
Example: io.helidon.security.providers.oidc.common.OidcConfig
- We do not use the verb, e.g. when a property "port" exists, the following methods are used:
- port(int newPort)
- int port()
- Boolean depends on how well understood the method is
- Default is without a verb (e.g. authenticate(boolean atn), boolean authenticate())
- If this would be ambiguous, we can use verb to clear the meaning (e.g. isAuthenticated() or shouldAuthenticate())
Example: io.helidon.security.providers.oidc.common.OidcConfig
- We use fluent API where applicable
- In builders (see builders section below)
- When using control methods (such as Server server = Server.create().start())
- We use builders to create instances that need any parameters for construction
- This implies that there are no public API classes that would use public constructors
- Allowed exceptions to this rule:
- Integration APIs that follow rules of integrated solution, e.g. Jersey SecurityFeature
- APIs that must be capable of reflection instantiation by tools that only support public constructors
- Exceptions with constructors for string, and string and a throwable
- Class or interface using a builder (let's call ours "FooBar" for the purpose of this document)
- Must have:
- Hidden constructor (private or protected) - this is to allow us to switch to interface if needed
- method public static Builder builder()
- all fields obtained from builder declared as final (immutable)
- May have:
- method public static Builder builder(?) - builder with mandatory or very commonly used parameters
- there should be a very small number of "builder(*)" methods - up to two per class
- Factory method public static FooBar create() that is implemented as "return builder().build()"
- Factory method public static create(io.helidon.config.Config config) that is implemented as "return builder().config(config).build()"
- Other factory methods that build specific (predefined) instances, such as "fail(String cause)", "success(Subject subject)" etc. - these methods MUST use builder to build the instance internally
- An internal class named "Builder" that is building instances of the containing class
- it is allowed to have the builder as a top level class, in such a case the name must reflect the class it is building (e.g. FooBarBuilder)
- method public static Builder builder(?) - builder with mandatory or very commonly used parameters
- Must have:
- Builder class
- Must have:
- Implements "io.helidon.common.Builder"
- All methods configuring the builder return a builder instance with updated configuration
- Validation done either on setters or in method build() (latest) - e.g. we should fail to build an instance if the configuration is not correct
- May have:
- May accept other classes that are built using a builder, either directly, or as Supplier (as builder implements Supplier, this allows us to pass a builder to such a method, as well as a nice lambda)
- Must have:
Example: io.helidon.security.providers.oidc.common.OidcConfig
- Each java module has a module-info.java defined in a "java9" directory under src/main
- These are compiled as part of a build when run on a JDK version 9 or higher
- We only use java 8 source compatibility (with the sole exception of module-info.java mentioned above)
- See useful helpers in common that provide some of the new nice features backported to java 8
We use JUnit 5 with Hamcrest assertions.
The Hamcrest assertion API differs a lot from JUnit assertion API and when both are used, the tests are hard to read.
We have chosen the Hamcrest approach:
- assertThat(actualValue, assertion(expectedValue))
Main import: import static org.hamcrest.MatcherAssert.assertThat;
- the main method to be used
Most commonly used assertions are available in org.hamcrest.CoreMatchers
:
- is() - assertion of equality:
assertThat(value, is(true)))
- notNullValue() - assertion of a non-null value:
assertThat(value, notNullValue())
- nullValue() - assertion of null value:
assertThat(value, nullValue())
- startsWith(String) - assertion that string is prefixed:
assertThat(value, startWith("a"))
- endsWith(String) - assertion that string is suffixed:
assertThat(value, endsWith("b"))
For other nice assertions see the CoreMatchers class or google ;)
The following assertions may be used from JUnit 5:
import static org.junit.jupiter.api.Assertions.assertAll;
- doing multiple assertions at once (more than one may fail)import static org.junit.jupiter.api.Assertions.assertThrows;
- asserting that an expression throws an exception
Example of why we have this rule:
Original assertion:
assertTrue(ex.getMessage().contains("'" + config.key() + "'"));
The output of the test was:
Expected: true, actual: false
Refactored assertion:
assertThat(ex.getMessage(), containsString("'" + config.key() + "'"));
New output:
Expected: a string containing "'list-1'"
but: was "Requested value for configuration key 'list-1.1' is not present in the configuration."
- All third party versions are managed in root pom
- Plugins
- Dependencies
- Adding a new third party dependency (or upgrading to a newer version) requires an internal process to be carried out. In such cases a delay is to be expected when merging.
- When referencing other Helidon modules, use ${project.version} as a version
- In pom.xml of a module, always define a "name" tag and follow naming conventions already used
- For project module: Helidon ${module_name} Project
- For java module: Helidon ${module_name}
- The name should be "reactor" friendly - it should not overflow
- The name is a name, not a sentence - it does not have to be grammatically correct
- All java modules that are expected to be used by our users MUST be defined in our bom pom
- Bundles may be created, though we still must give our users the freedom to pick and choose modules directly
- Avoid bundling third party dependencies that may bring unexpected libraries in (e.g. Google Login provider)
- $root/bundles - SE bundles (groupId: io.helidon.bundles)
- microprofile/bundles - MP bundles
- Bundles are for end users, not for internal use
- Java EE components and Microprofile specifications should be in "provided" scope unless you are implementing
the spec itself
- Analyze the dependencies of your module and choose the correct maven scope and module-info.java dependency declaration
- Mapping to module-info.java
- compile -> requires
- optional -> requires static
- provided -> requires
- runtime -> "requires" or "requires static" depending on requirements note that "requires static" only works if the module is required by any other module used, otherwise it does not end up on module path even if it is on the class path
- Use transitive in module-info.java for your dependencies that are part of public API of the module
- Carefully choose scope for dependencies on other helidon modules (e.g. microprofile extensions should have helidon microprofile in "provided" scope)