Skip to content

Commit

Permalink
Add variables to Message (#903)
Browse files Browse the repository at this point in the history
* Add variables to Messages

This change adds MessageResource and LocalizedMessageResolver APIs to ease resolving a message given a instance of io.micronaut.views.fields.messages.Message

* improves compareTo
  • Loading branch information
sdelamo authored Nov 7, 2024
1 parent 65d4d19 commit 1e9aa3c
Show file tree
Hide file tree
Showing 8 changed files with 299 additions and 4 deletions.
1 change: 1 addition & 0 deletions views-fieldset/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ dependencies {
compileOnly(projects.micronautViewsCore)
testImplementation(mnData.micronaut.data.model)
testImplementation(mn.micronaut.http)
testImplementation(mn.micronaut.http.server.netty)
testAnnotationProcessor(mn.micronaut.inject.java)
testImplementation(mnTest.junit.jupiter.api)
testImplementation(mnTest.micronaut.test.junit5)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
/*
* Copyright 2017-2024 original authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package io.micronaut.views.fields.messages;

import io.micronaut.context.LocalizedMessageSource;
import io.micronaut.core.annotation.NonNull;
import java.util.Optional;

/**
* Utility class to resolve message of a {@link Message} with a given {@link LocalizedMessageSource}.
* @author Sergio del Amo
* @since 5.6.0
*/
public class LocalizedMessageResolver {

private final LocalizedMessageSource messageSource;

public LocalizedMessageResolver(LocalizedMessageSource messageSource) {
this.messageSource = messageSource;
}

/**
*
* @param message The message
* @return the resolved message or the defaultMessage if the message is not found.
*/
@NonNull
public String getMessageOrDefault(@NonNull Message message) {
return messageSource.getMessageOrDefault(message.code(), message.defaultMessage(), message.variables());
}

/**
*
* @param message The message
* @return the resolved message or an empty optional if not found.
*/
@NonNull
public Optional<String> getMessage(@NonNull Message message) {
return messageSource.getMessage(message.code(), message.variables());
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -25,23 +25,52 @@
import jakarta.validation.constraints.NotBlank;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Objects;

/**
* Message representation. It features an optional {@link Message#code()} to allow localization.
* @author Sergio del Amo
* @since 4.1.0
* @param defaultMessage The default message to use if no code is specified or no localized message found.
* @param code The i18n code which can be used to fetch a localized message.
* @param variables The variables to use resolve message placeholders
*/
@Experimental
@Introspected
public record Message(@NonNull @NotBlank String defaultMessage,
@Nullable String code) implements Comparable<Message> {
@Nullable String code,
@Nullable Object[] variables) implements Comparable<Message> {
private static final String REGEX = "(.)([A-Z])";
private static final String REPLACEMENT = "$1 $2";
private static final String DOT = ".";

public Message(@NonNull String defaultMessage, @Nullable String code) {
this(defaultMessage, code, null);
}

@Override
public boolean equals(Object o) {
if (this == o) {
return true;
}
if (o == null || getClass() != o.getClass()) {
return false;
}
Message message = (Message) o;
return defaultMessage.equals(message.defaultMessage) &&
Objects.equals(code, message.code) &&
Arrays.equals(variables, message.variables);
}

@Override
public int hashCode() {
int result = Objects.hash(defaultMessage, code);
result = 31 * result + Arrays.hashCode(variables);
return result;
}

@Override
public int compareTo(Message o) {
int compare = defaultMessage().compareTo(o.defaultMessage());
Expand All @@ -54,10 +83,30 @@ public int compareTo(Message o) {
if (this.code() != null && o.code() == null) {
return 1;
}
if (this.code() == null) {
return 0;
if (this.code() != null) {
compare = this.code().compareTo(o.code());
if (compare != 0) {
return compare;
}
}
return this.code().compareTo(o.code());
if (this.variables == null && o.variables != null) {
return -1;
}
if (this.variables != null && o.variables == null) {
return 1;
}
if (this.variables != null) {
for (int i = 0; i < Math.min(this.variables.length, o.variables.length); i++) {
if (this.variables[i] instanceof Comparable variablesComparable) {
compare = variablesComparable.compareTo(o.variables[i]);
if (compare != 0) {
return compare;
}
}
}
return Integer.compare(this.variables.length, o.variables.length);
}
return 0;
}

/**
Expand All @@ -72,6 +121,20 @@ public static Message of(@NonNull String defaultMessage,
return new Message(defaultMessage, code);
}

/**
*
* @param defaultMessage The default message to use if no code is specified or no localized message found
* @param code The i18n code which can be used to fetch a localized message.
* @param variables The variables to use resolve message placeholders
* @return A {@link Message} instance.
*/
@NonNull
public static Message of(@NonNull String defaultMessage,
@Nullable String code,
@Nullable Object... variables) {
return new Message(defaultMessage, code, variables);
}

/**
*
* @param defaultMessage The default message to use if no code is specified or no localized message found
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
/*
* Copyright 2017-2024 original authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package io.micronaut.views.fields.messages;

import io.micronaut.context.MessageSource;
import io.micronaut.core.annotation.NonNull;

import java.util.Locale;
import java.util.Optional;

/**
* Utility class to resolve message of a {@link Message} with a given {@link MessageSource}.
* @author Sergio del Amo
* @since 5.6.0
*/
public class MessageResolver {

private final MessageSource messageSource;

public MessageResolver(MessageSource messageSource) {
this.messageSource = messageSource;
}

/**
*
* @param message The message
* @param locale the Locale
* @return the resolved message or the defaultMessage if the message is not found.
*/
@NonNull
public String getMessageOrDefault(@NonNull Message message, @NonNull Locale locale) {
return messageSource.getMessage(message.code(), message.defaultMessage(), locale, message.variables());
}

/**
*
* @param message The message
* @param locale the Locale
* @return the resolved message or an empty optional if not found.
*/
@NonNull
public Optional<String> getMessage(@NonNull Message message, @NonNull Locale locale) {
return messageSource.getMessage(message.code(), locale, message.variables());
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
/*
* Copyright 2017-2024 original authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package io.micronaut.views.fields.messages;

import io.micronaut.context.LocalizedMessageSource;
import io.micronaut.context.MessageSource;
import io.micronaut.context.annotation.Factory;
import io.micronaut.context.annotation.Requires;
import io.micronaut.core.annotation.Internal;
import jakarta.inject.Singleton;

@Factory
@Internal
final class MessageResolverFactory {

@Singleton
MessageResolver createMessageResolver(MessageSource messageSource) {
return new MessageResolver(messageSource);
}

@Requires(beans = LocalizedMessageSource.class)
@Singleton
LocalizedMessageResolver createMessageResolver(LocalizedMessageSource messageSource) {
return new LocalizedMessageResolver(messageSource);
}
}
Original file line number Diff line number Diff line change
@@ -1,13 +1,41 @@
package io.micronaut.views.fields.messages;

import io.micronaut.context.AbstractLocalizedMessageSource;
import io.micronaut.context.MessageSource;
import io.micronaut.context.annotation.*;
import io.micronaut.context.i18n.ResourceBundleMessageSource;
import io.micronaut.core.annotation.Introspected;
import io.micronaut.core.annotation.NonNull;
import io.micronaut.core.util.LocaleResolver;
import io.micronaut.http.HttpRequest;
import io.micronaut.http.server.util.locale.HttpLocalizedMessageSource;
import io.micronaut.test.extensions.junit5.annotation.MicronautTest;
import io.micronaut.validation.validator.Validator;
import jakarta.inject.Inject;
import jakarta.inject.Singleton;
import jakarta.validation.ConstraintViolationException;
import jakarta.validation.Valid;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotNull;
import org.junit.jupiter.api.Test;

import java.util.Locale;

import static org.junit.jupiter.api.Assertions.*;

@Property(name = "spec.name", value = "MessageTest")
@MicronautTest(startApplication = false)
class MessageTest {
private static final Locale ES = new Locale("es");

@Inject
MessageSource messageSource;

@Inject
MessageResolver messageResolver;

@Inject
LocalizedMessageResolver localizedMessageResolver;

@Test
void messageValidation(Validator validator) {
Expand All @@ -17,4 +45,54 @@ void messageValidation(Validator validator) {
String msg = null;
assertFalse(validator.validate(Message.of(msg)).isEmpty());
}

@Test
void twoArgumentConstructor() {
assertEquals(new Message("Foo", "foo.code"), Message.of("Foo", "foo.code"));
}

@Test
void threeArgumentConstructor() {
String code = "default.null.message";
String defaultMessage = "Property [{0}] of class [{1}] cannot be null";
Message message = Message.of(defaultMessage, code, "pages", "Book");
Message expected = new Message(defaultMessage, code, new Object[]{"pages", "Book"});
assertEquals(expected, message);
}

@Test
void messageSource() {
String code = "default.null.message";
String defaultMessage = "Property [{0}] of class [{1}] cannot be null";
Message message = Message.of(defaultMessage, code, "pages", "Book");
assertEquals("La propiedad [pages] de la clase [Book] no puede ser nulo",
messageSource.getMessage(message.code(), message.defaultMessage(), ES, message.variables()));
assertEquals("La propiedad [pages] de la clase [Book] no puede ser nulo",
messageResolver.getMessageOrDefault(message, ES));
assertEquals("La propiedad [pages] de la clase [Book] no puede ser nulo",
localizedMessageResolver.getMessageOrDefault(message));
}

@Requires(property = "spec.name", value = "MessageTest")
@Factory
static class MessageSourceFactory {
@Singleton
MessageSource createMessageSource() {
return new ResourceBundleMessageSource("i18n.messages");
}
}

@Requires(property = "spec.name", value = "MessageTest")
@Primary
@Singleton
static class HttpLocalizedMessageSourceReplacement extends AbstractLocalizedMessageSource {
HttpLocalizedMessageSourceReplacement(LocaleResolver<HttpRequest<?>> localeResolver, MessageSource messageSource) {
super(localeResolver, messageSource);
}

@Override
protected @NonNull Locale getLocale() {
return ES;
}
}
}
1 change: 1 addition & 0 deletions views-fieldset/src/test/resources/i18n/messages.properties
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
default.null.message=Property [{0}] of class [{1}] cannot be null
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
default.null.message=La propiedad [{0}] de la clase [{1}] no puede ser nulo

0 comments on commit 1e9aa3c

Please sign in to comment.