Hawaii Features
TODO.
Environments
TODO.
Configuration properties
TODO.
Logging
The Hawaii logging feature provides a number of filter beans which add information that can log information about requests. The Hawaii logging is only usable in a servlet environment, because all filters use HttpServletRequest and HttpServletResponse to obtain information.
The main purpose of Hawaii logging is to write structured information into log lines, in such a way that it can be parsed by elastic search (Kibana). A specialized encoder is included to write out the log lines, as well as a bunch of filters that gather and, in some cases, create information to be included in each message. Hawaii logging currently only contains a logback-based encoder, so Hawaii logging is only usable if you use logback as your logging implementation.
The next sections describe the individual filters contained in the Hawaii logging module. For more information on disabling and configuring individual filters, please refer to the Hawaii logging starter.
Kibana
The Kibana log filter gathers request data such as request method, uri and client ip address and stores it so it can be added to subsequent log messages.
Kibana cleanup
The Kibana cleanup filter empties all Kibana-specific data after the rest of the filter chain has been processed. It is intended to be used early in the chain, such that the cleanup happens last.
Request duration
This filter determines the duration of the request in milliseconds. It add the duration to the Kibana fields and logs it.
Request id
This filter generates a new request id, adds it to the Kibana fields and also writes it as a header on the response.
Transaction id
This filter reads an incoming transaction id from a request header, or generates a new one. The transaction id is written to a response header and added to the Kibana fields. The ability to read an incoming transaction id provides the ability to have multiple requests, spanning multiple systems within the same transaction.
User details
This filter retrieves the authenticated principal from the security context and stores the user name in the Kibana fields so that it gets included in each log message. This filter is only instantiated if Spring security is found on the class path.
Request/response
This filter logs the content type, size, headers and body of incoming requests, as well as the response status, headers and body. Optionally, the filter can log the response to a file if it exceeds a certain threshold size.
Hawaii Time
HawaiiTime
is not merely a convenient wrapper to instantiate new java.time
date and time objects.
It provides an application wide java.time.Clock
reference which is particular useful for unit testing.
It is similar to Joda’s DateTimeUtils
which also allows setting a fixed current time.
However it is important to note that Joda’s DateTimeUtils
uses a static variable to store the current time.
HawaiiTime
does not take this approach. Instead the HawaiiTime
bean needs to be injected in any class that needs to instantiate new date
and time objects. This approach is more flexible and e.g. has the benefit that unit tests can be run in parallel. See example usage below.
public class MyClass {
private HawaiiTime hawaiiTime;
public MyClass(HawaiiTime hawaiiTime) { # (1)
this.hawaiiTime = hawaiiTime;
}
public void doSomethingWithDate() {
ZonedDateTime dateTime = this.hawaiiTime.zonedDateTime(); # (2)
// ...
}
}
public class MyClassTests {
@Test
public void testDoSomethingWithDate() {
long millis = System.currentTimeMillis();
HawaiiTime hawaiiTime = new HawaiiTime();
hawaiiTime.useFixedClock(millis); # (3)
MyClass myClass = new MyClass(hawaiiTime);
myClass.doSomethingWithDate();
// ...
}
}
-
Inject the
HawaiiTime
bean. -
Use the injected
HawaiiTime
bean to instantiate new date and time objects. -
In unit tests a fixed clock can be used to manipulate and predict the exact current time.
Another benefit of using HawaiiTime
is that a fixed time can be used in a running application to test how it behaves on a given date or
time.
Caution
|
Third-party libraries being used by the application do not use HawaiiTime and probably instantiate date and time objects based on
the System time.
|
Hawaii uses UTC
as default timezone but this can be changed by setting the hawaii.time.timezone
configuration property. The provided
value will be parsed by java.time.ZoneId#of(String zoneId)
and supports different timezone formats like UTC
, Europe/Amsterdam
and
GMT+1
.
The creation of the HawaiiTime
bean can also be disabled by setting hawaii.time.enabled
to false
.
Validation
Hawaii’s validation mechanism can be used to validate any object. It basically validates values, collects validation errors and stores them in a validation result. These validation errors are simple field / error code combinations.
Hawaii’s Validator
is inspired on Spring’s org.springframework.validation.Validator
mechanism. However Hawaii’s validator mechanism
uses it’s own ValidationResult
instead of Spring’s org.springframework.validation.Errors
. The main difference is that Hawaii’s
ValidationResult
does not bind directly the object being validated. This also gives the possibility to add errors for specific keys that
are not direct properties of the object being validated.
Hawaii’s validation mechanism also provides additional sugar like Hamcrest matcher support to write human readable validating code,
the capability to validate and automatically throw a ValidationException
in case of errors etc.
Like Spring’s validation mechanism the Hawaii validation mechanism also supports the notion of nested error paths which also stimulates to re-use validators.
Let’s take an example. Imagine a Customer
object with common name, e-mail, and address fields. A validation result could for example
contain the following field / error code combinations:
first_name = required # (1)
last_name = max_length_exceeded
email = invalid
addresses = primary_address_required # (2)
addresses[0].type = invalid # (3)
addresses[0].street_name = max_length_exceeded
addresses[0].postal_code = invalid
addresses[0].city = max_length_exceeded
addresses[0].country_code = required
-
The field
first_name
has anrequired
error code. -
The field
adresses
(an array in this case) hasprimary_address_required
error code. -
The field
type
of the first address in theaddresses
array has ainvalid
error code.
The example demonstrates simple field errors (like first_name
) but also storing errors for arrays and nested paths (addresses[0].type
).
In theory a field could also have multiple error codes if needed.
Implementors should typically only implement the org.hawaiiframework.sample.validator.Validator#validate(Object, ValidationResult)
method
as the other methods in the interface are already implemented using the interface’s default methods.
A generic EmailValidator
would look like:
import org.hawaiiframework.validation.ValidationResult;
import org.hawaiiframework.validation.Validator;
import org.springframework.stereotype.Component;
import java.util.regex.Pattern;
@Component
public class EmailValidator implements Validator<String> { # (1)
public static final String EMAIL_PATTERN = "^[_A-Za-z0-9-\\+]+(\\.[_A-Za-z0-9-]+)*@[A-Za-z0-9-]+(\\.[A-Za-z0-9]+)*(\\.[A-Za-z]{2,})$";
private Pattern pattern;
public EmailValidator() {
this.pattern = Pattern.compile(EMAIL_PATTERN);
}
@Override
public void validate(String email, ValidationResult validationResult) { # (2)
if (!pattern.matcher(email).matches()) {
validationResult.rejectValue("invalid"); # (3)
}
}
}
-
Implement the
Validator
(in this case aString
). -
Override the
Validator#validate(Object, ValidationResult)
method. -
In case the e-mail is invalid, reject the value with error code
invalid
and store it in the validation result.
The CustomerValidator
would look like:
import org.apache.commons.lang3.StringUtils;
import org.hawaiiframework.sample.validator.EmailValidator;
import org.hawaiiframework.validation.ValidationResult;
import org.hawaiiframework.validation.Validator;
import org.springframework.stereotype.Component;
import java.util.List;
import static org.hamcrest.Matchers.greaterThan;
@Component
public class CustomerInputValidator implements Validator<CustomerInput> { # (1)
private final EmailValidator emailValidator;
private final AddressValidator addressValidator;
public CustomerInputValidator(final EmailValidator emailValidator,
final AddressValidator addressValidator) { # (2)
this.emailValidator = emailValidator;
this.addressValidator = addressValidator;
}
@Override
public void validate(CustomerInput customer, ValidationResult validationResult) { # (3)
// first name validation
String firstName = customer.getFirstName();
if (StringUtils.isBlank(firstName)) {
validationResult.rejectValue("first_name", "required");
} else {
validationResult.rejectValueIf(firstName.length(), greaterThan(25), "first_name",
"max_length_exceeded");
}
// last name validation
String lastName = customer.getLastName();
if (StringUtils.isBlank(lastName)) {
validationResult.rejectValue("last_name", "required");
} else {
validationResult.rejectValueIf(lastName.length(), greaterThan(25), "last_name",
"max_length_exceeded");
}
// e-mail validation
String email = customer.getEmail();
if (StringUtils.isBlank(email)) {
validationResult.rejectValue("email", "required");
} else if (email.length() > 100) {
validationResult.rejectValue("email", "max_length_exceeded");
} else {
validationResult.pushNestedPath("email");
emailValidator.validate(email, validationResult);
validationResult.popNestedPath();
}
// adresses validation
List<Address> addresses = customer.getAddresses();
if (addresses == null || addresses.size() == 0) {
validationResult.rejectValue("addresses", "required");
} else {
// addresses array validations
long primaries = addresses.stream()
.filter(address -> address.getType() == AddressType.PRIMARY)
.count();
if (primaries == 0) {
validationResult.rejectValue("addresses", "primary_address_required");
} else if (primaries > 1) {
validationResult.rejectValue("addresses", "only_1_primary_address_allowed");
}
if (addresses.size() > 3) {
validationResult.rejectValue("addresses", "max_array_length_exceeded");
}
// address validations
for (int i = 0; i < addresses.size(); i++) {
validationResult.pushNestedPath("addresses", i);
addressValidator.validate(addresses.get(i), validationResult);
validationResult.popNestedPath();
}
}
}
}
-
Implement the
Validator
(in this case aCustomer
). -
Inject other validators (
EmailValidator
,AddressValidator
) to be re-used. -
Override the
Validator#validate(Object, ValidationResult)
method.
Web
Global Exception Handler
TODO.
REST Representations
TODO.
Input Converter
TODO.
Resource Assembler
TODO.