Author: James R. Perkins

# JBoss Logging Tools

James R. Perkins
<jperkins@redhat.com>
version 2.1.0.Final

## Introduction

The JBoss logging tools are used to create internationalized log statements and exceptions.

## Getting Started

To get started you need to include three dependencies in your project.

JBoss Logging | Required at compile time and runtime.
JBoss Logging Annotations | Required at compile time only.
JBoss Logging Processor | Required at compile time only.

If you’re using maven here is an example `pom.xml` snippet:

    <dependencies>
        <!-- Required by the annotation processor and will be used at runtime -->
        <dependency>
            <groupId>org.jboss.logging</groupId>
            <artifactId>jboss-logging</artifactId>
        </dependency>

        <dependency>
            <groupId>org.jboss.logging</groupId>
            <artifactId>jboss-logging-annotations</artifactId>
            <version>2.1.0.Final</version>
            <!-- This is a compile-time dependency of this project, but is not needed at compile or runtime by other
                  projects that depend on this project.-->
            <scope>provided</scope>
            <optional>true</optional>
        </dependency>

        <dependency>
            <groupId>org.jboss.logging</groupId>
            <artifactId>jboss-logging-processor</artifactId>
            <version>2.1.0.Final</version>
            <!-- This is a compile-time dependency of this project, but is not needed at compile or runtime by other
                  projects that depend on this project.-->
            <scope>provided</scope>
            <optional>true</optional>
        </dependency>
    </dependencies>

Once your project is configured you can create either a message bundle interface or a message logger interface. For detailed information see the JavaDocs for the annotations.

## Message Bundle Interfaces

Message bundle interfaces provide a way to internationalize exceptions or strings. A message bundle interface is annotated with `@MessageBundle`. Each method in must be annotated with `@Message` which will be used to determine the String used for either the return type or the message for the exception being returned.

The value for a `@Message` may contain an expression.

| Expressions are in the form of `${key:defaultValue}`. If the key is prefixed with `sys.` a system property is used. If the key is prefixed with `env.` an environment variable is used. In all other cases the properties are resolved from the `org.jboss.logging.tools.expressionProperties` path. If the key is not found in the properties the default value will be used.

| Expressions are processed at compile time. The values will be hard-coded in the generated source files.

The following constraints are placed on methods in a message bundle:

- The return type must be one of the follow

  - A `java.lang.String`

  - A `java.lang.Throwable` or a subtype of `java.lang.Throwable`

  - A `java.lang.function.Supplier` who’s return type fits one of the other two constraints above.

- The method must be annotated with `@Message` or a message must be inheritable.

- A method can have only one `@Cause` parameter.

- A method can only have one `@Producer` parameter.

| A message is inheritable if the two methods have the same name and same number of format parameters.

| A format parameter is a parameter that has no annotations or is annotated with one of the following annotations; `@FormatWith`, `@Pos` or `@Transform`.

### Example Message Bundle

    @MessageBundle(projectCode = "CW") (1)
    public interface ErrorMessages {
        ErrorMessages MESSAGES = Messages.getBundle(ErrorMessages.class);

        @Message("Version %d.%d.%d.%s") (2)
        String version(int major, int minor, int macro, String rel);

        @Message(id = 1, value = "Value '%s' is invalid")
        IllegalArgumentException invalidValue(Object value);

        @Message(id = 2, value = "Failure closing %s") (3)
        CloseException closeFailure(@Cause Throwable cause, @Param @Pos(1) Closeable c);

        CloseException closeFailure(@Cause Throwable cause, @Param @Pos(1) Closeable c, @Suppressed Throwable... errors);

        @Message(id = 3, value = "Parameter %s cannot be null")
        Supplier<String> nullParam(String name);

        @Message(id = 4, value = "Operation %s failed.")
        <T extends RuntimeException> T operationFailed(@Producer Function<String, T> fn, String name); (4)

        <T extends RuntimeException> T operationFailed(@Producer BiFunction<String, IOException, T> fn, @Cause IOException cause, String name);
    }

1 | The `projectCode` will be prepended to messages which have an `id` specified. For example with `id = 1` the message will be prepended with `CW000001`. You can control the number padding with the `length` property on the annotation.
2 | No `id` is specified for this message which means no id will be prepended on this message.
3 | The `@Param` annotation tells the generator that the parameter should be used to construct the `CloseException`. The `@Pos(1)` annotation indicates the parameter should also be used when formatting the message.
4 | The `@Producer` annotation indicates that the `Function` should be used to create the exception being returned.

| Message bundle interfaces can also contain valid message logger methods.

## Message Logger Interfaces

Logger interfaces provide a way to internationalize log messages. A logger interface is an interface annotated with `@MessageLogger`. Each method in must be annotated with `@Message` which will be used to determine the message to be logged.

The value for a `@Message` may contain an expression.

| Expressions are in the form of `${key:defaultValue}`. If the key is prefixed with `sys.` a system property is used. If the key is prefixed with `env.` an environment variable is used. In all other cases the properties are resolved from the `org.jboss.logging.tools.expressionProperties` path. If the key is not found in the properties the default value will be used.

| Expressions are processed at compile time. The values will be hard-coded in the generated source files.

The following constraints are placed on methods in a message logger:

- The method must have a `void` return type

- The method must be annotated with `@LogMessage`

- The method must be annotated with `@Message` or a message must be inheritable.

- A method can have only one `@Cause` parameter.

| A message is inheritable if the two methods have the same name and same number of format parameters.

| A format parameter is a parameter that has no annotations or is annotated with one of the following annotations; `@FormatWith`, `@Pos` or `@Transform`.

A message logger interface may also extend the `org.jboss.logging.BasicLogger`. This allows the logging interface to be usable for standard logging. For example `AppLogger.LOGGER.debugf("Initializing %s", this);`

### Example Message Logger

    @MessageLogger(projectCode = "CW") (1)
    public interface AppLogger extends BasicLogger {

        AppLogger LOGGER = Logger.getMessageLogger(AppLogger.class, AppLogger.class.getPackage().getName());

        @LogMessage
        @Once (2)
        @Message("%s version %d.%d.%d.%s") (3)
        void appVersion(CharSequence name, int major, int minor, int macro, String rel);

        @LogMessage(level = Logger.Level.ERROR) (4)
        @Message(id = 100, value = "Failure while closing %s")
        void closeFailure(@Cause Throwable cause, Object obj);

        @LogMessage(level = Logger.Level.WARN)
        @Message(id = 101, value = "Encoding %s could not be found. Defaulting to %s.")
        void encodingNotFound(String encoding, Charset dft);

        @LogMessage
        @Message(id = 102, value = "Cache size changed to '%d'")
        void cacheSizeChanged(@Transform(Transform.TransformType.SIZE) Collection<String> c);

        @LogMessage
        void cacheSizeChanged(@Transform(Transform.TransformType.SIZE) String... array);

        @LogMessage
        void cacheSizeChanged(@Transform(Transform.TransformType.SIZE) Map<String, Object> map);
    }

1 | The `projectCode` will be prepended to messages which have an `id` specified. For example with `id = 100` the message will be prepended with `CW000100`. You can control the number padding with the `length` property on the annotation.
2 | Ensures the log message is only written once.
3 | No `id` is specified for this message which means no id will be prepended on this message.
4 | Overrides the default level to `ERROR` to indicate an error message should be logged.

## Reports

There are currently two types of reports that can be generated. The options are `adoc` or `asciidoc` for asciidoc and `xml` for XML.

The reports contain the following data:

- The formatted message id.

- A possible link to a resolution document for the error.

- The unformatted message.

- The log level, if applicable.

- The return type, if applicable.

### Annotations

Two annotations can be used to assist withing linking resolution documents for messages.

- `@BaseUrl`

  - This annotation can be used on a type to define the base URL for linking resolution documents. This annotation is not required for links to be created on reports.

- `@ResolutionDoc`

  - This annotation is used to define information about the resolution document for creating links. If placed on an interface all methods that have a defined id will have a link generated. This can also be placed individually on the method to only generate links for specific id’s.

## Example Use Cases

Below are some example use case snippets from the examples.

    /**
     * Writes the value of the object to the file.
     *
     * @param value the value to write, cannot be {@code null}
     *
     * @throws UncheckedIOException if an error occurs writing the data
     */
    public void write(final Object value) {
        AppLogger.LOGGER.appVersion("ContentWriter", 1, 0, 0, "Beta1"); (1)
        Objects.requireNonNull(value, ErrorMessages.MESSAGES.nullParam("value")); (2)
        write(Objects.requireNonNull(value, ErrorMessages.MESSAGES.nullParam("value")).toString());
    }

    /**
     * Writes the value to the file.
     *
     * @param value the value to write, cannot be {@code null} or an {@linkplain String#isEmpty() empty string}.
     *
     * @throws UncheckedIOException if an error occurs writing the data
     */
    public void write(final String value) {
        AppLogger.LOGGER.appVersion("ContentWriter", 1, 0, 0, "Beta1");
        if (Objects.requireNonNull(value, ErrorMessages.MESSAGES.nullParam("value")).isEmpty()) {
            throw ErrorMessages.MESSAGES.invalidValue(value); (3)
        }
        try {
            synchronized (outputLock) {
                writer.write(value);
                writer.newLine();
                if (autoFlush) {
                    flush();
                }
            }
        } catch (IOException e) {
            throw ErrorMessages.MESSAGES.operationFailed(UncheckedIOException::new, e, "write"); (4)
        }
    }

    @Override
    public void close() {
        try {
            synchronized (outputLock) {
                writer.close();
            }
            AppLogger.LOGGER.tracef("ContentWriter %s was successfully closed.", this);
        } catch (Exception e) {
            throw ErrorMessages.MESSAGES.closeFailure(e, this);
        }
    }

    /**
     * Safely close this writer logging any errors that occur during closing.
     */
    public void safeClose() {
        try {
            synchronized (outputLock) {
                writer.close();
            }
            AppLogger.LOGGER.tracef("ContentWriter %s was successfully closed.", this);
        } catch (Exception e) {
            AppLogger.LOGGER.closeFailure(e, this); (5)
        }
    }

1 | Logs the application version. Note this uses the @Once annotation to indicate this should only be logged once regardless of which `write` method is used.
2 | The `ErrorMessages.nullParam()` returns a `java.lang.function.Supplier`. This allows the message to be lazily formatted only if the `value` is `null`.
3 | Throws a message if the `value` is an empty string.
4 | Uses a `java.lang.function.BiFunction` to create a new `UncheckedIOException` with the caught exception set as the cause.
5 | Logs the caught exception instead of throwing a new exception.

## Translation Property Files

The translation properties files must exist in the same directory structure as the interface. The name of the properties file `InterfaceName.i18n_language_country_variant.properties`. For example if you have a class named `org.jboss.logging.tools.examples.ErrorMessages` and you want to translate this into French you create a properties file called `ErrorMessages.i18n_fr.properties` in a directory `org/jboss/logging/tools/examples`.

## Annotation Processor Options

You can pass options to an annotation processor with the `-A` compiler option. For example to disable generating the `@Generated` annotation from being placed on the generated source files you would pass `-Aorg.jboss.logging.tools.addGeneratedAnnotation=false` to the compiler command.

### General

Option | Description
--- | ---
`debug` | This option turns on debug logging for the processor
`org.jboss.logging.tools.expressionProperties` | This option allows you to define a path where, at compile-time, expressions in messages can be resolved.
`org.jboss.logging.tools.addGeneratedAnnotation` | If set to `false` the `@Generated` annotation will not be placed on the generated source files. The default is `true`.

| In Java 9 the `@javax.annotation.Generated` was moved to `@javax.annotation.processor.Generated`. The processor attempts to determine which annotation to use by attempting to find the `@javax.annotation.Generated` first. If it fails the `@javax.annotation.processor.Generated` is attempted. If neither can be found no annotation will be placed on the generated implementations.

| Expressions are in the form of `${key:defaultValue}`. If the key is prefixed with `sys.` a system property is used. If the key is prefixed with `env.` an environment variable is used. In all other cases the properties are resolved from the `org.jboss.logging.tools.expressionProperties` path. If the key is not found in the properties the default value will be used.

| Expressions are processed at compile time. The values will be hard-coded in the generated source files.

### Translation Options

Option | Description
--- | ---
`translationsFilesPath` | The base path for the translated properties files. This defaults to the location where new class files are placed.
`skipTranslations` | If set to `true` source files with the translated tet will not be generated. The default is `false`.
`generatedTranslationFilesPath` | If defined this indicates the path a skeleton file should be generated for the interface. The generated skeleton file will be placed in a directory that matches the package with a name that matches the interface with a `.i18n_locale_COUNTRY_VARIANT.properties` suffix.
`org.jboss.logging.tools.level` | Sets the maximum level to include in the generated skeleton files. For example if set to `INFO` the skeleton files will not contain any properties where the log level was set to `DEBUG` or `TRACE`.

### Report Options

Option | Description
--- | ---
`org.jboss.logging.tools.report.type` | Indicates the type of report that should be generated. The current report types are `adoc` for asciidoc or `xml` for XML.
`org.jboss.logging.tools.report.path` | The path where the generated reports should be placed. This defaults to the location where new class files are placed.
`org.jboss.logging.tools.report.title` | An optional title for the report. For asciidoc this defaults to `Messages`. For XML the `<title>` element is left off.