Skip to content

Commit

Permalink
Merge pull request #196 from croz-ltd/feature_addSupportForResolvingN…
Browse files Browse the repository at this point in the history
…otificationContentFromMessageOrMessageCode

Support resolving of notification content from exception message or message code
  • Loading branch information
jzrilic authored Mar 7, 2024
2 parents dcc3608 + e690ce7 commit 2ffe052
Show file tree
Hide file tree
Showing 15 changed files with 212 additions and 29 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
/*
* Copyright 2020-2023 CROZ d.o.o, the original author or 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 net.croz.nrich.core.api.exception;

/**
* Marker interface that uses exception message for notification.
*/
public interface ExceptionWithMessage {

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
/*
* Copyright 2020-2023 CROZ d.o.o, the original author or 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 net.croz.nrich.core.api.exception;

/**
* Implementing this interface enables resolving exception message from <pre>{@code MessageSource}</pre> using supplied message code.
*/
public interface ExceptionWithMessageCode {

String getMessageCode();

}
Original file line number Diff line number Diff line change
Expand Up @@ -55,10 +55,9 @@ public interface BaseNotificationResponseService<T> {
*
* @param throwable exception for which to resolve notification
* @param additionalNotificationData additional notification data to add to notification
* @param exceptionMessageArgumentList optional exception argument list, will be used when resolving exception message
* @return response with notification
*/
T responseWithExceptionNotification(Throwable throwable, AdditionalNotificationData additionalNotificationData, Object... exceptionMessageArgumentList);
T responseWithExceptionNotification(Throwable throwable, AdditionalNotificationData additionalNotificationData);

/**
* Returns response with {@link net.croz.nrich.notification.api.model.Notification} instance.
Expand Down Expand Up @@ -87,8 +86,8 @@ default T responseWithValidationFailureNotification(ConstraintViolationException
return responseWithValidationFailureNotification(exception, AdditionalNotificationData.empty());
}

default T responseWithExceptionNotification(Throwable throwable, Object... additionalMessageArgumentList) {
return responseWithExceptionNotification(throwable, AdditionalNotificationData.empty(), additionalMessageArgumentList);
default T responseWithExceptionNotification(Throwable throwable) {
return responseWithExceptionNotification(throwable, AdditionalNotificationData.empty());
}

default T responseWithNotificationActionResolvedFromRequest() {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -56,10 +56,9 @@ public interface NotificationResolverService {
*
* @param throwable exception for which to resolve notification
* @param additionalNotificationData additional notification data to add to notification
* @param exceptionMessageArgumentList optional exception argument list, will be used when resolving exception message
* @return {@link Notification} instance
*/
Notification createNotificationForException(Throwable throwable, AdditionalNotificationData additionalNotificationData, Object... exceptionMessageArgumentList);
Notification createNotificationForException(Throwable throwable, AdditionalNotificationData additionalNotificationData);

/**
* Returns {@link Notification} instance for action. Default severity is <pre>INFO</pre>. Resolved action message is added as notification content.
Expand All @@ -79,8 +78,8 @@ default ValidationFailureNotification createNotificationForValidationFailure(Con
return createNotificationForValidationFailure(exception, AdditionalNotificationData.empty());
}

default Notification createNotificationForException(Throwable throwable, Object... exceptionMessageArgumentList) {
return createNotificationForException(throwable, AdditionalNotificationData.empty(), exceptionMessageArgumentList);
default Notification createNotificationForException(Throwable throwable) {
return createNotificationForException(throwable, AdditionalNotificationData.empty());
}

default Notification createNotificationForAction(String actionName) {
Expand Down
12 changes: 11 additions & 1 deletion nrich-notification/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -263,7 +263,11 @@ In [`DefaultNotificationResolverService`][default-notification-resolver-service-

### Custom exception notification data

Depending on the context of notification creation, if a notification is created while resolving an exception, then the action code is a **fully qualified class name for that exception**.
Depending on the context of notification creation, if a notification is created while resolving an exception, then the default action code is a **fully qualified class name for that exception**.
If an exception implements [`ExceptionWithMessageCode`][exception-with-message-code-url] then notification content is resolved through provided message code
and if an exception implements [`ExceptionWithMessage`][exception-with-message-url] then notification content is equal to exceptions message
(title is still resolved from fully qualified class name in both cases). If additional arguments are required when resolving message, exception should implement
[`ExceptionWithArguments`][exception-with-arguments-url].

For example, let's say we have this exception handler:

Expand Down Expand Up @@ -500,3 +504,9 @@ where we can see that instead of `{0}` the value was interpolated.
[additional-notification-data-url]: ../nrich-notification-api/src/main/java/net/croz/nrich/notification/api/model/AdditionalNotificationData.java

[default-notification-resolver-service-url]: ../nrich-notification/src/main/java/net/croz/nrich/notification/service/DefaultNotificationResolverService.java

[exception-with-message-code-url]: ../nrich-core-api/src/main/java/net/croz/nrich/core/api/exception/ExceptionWithMessageCode.java

[exception-with-message-url]: ../nrich-core-api/src/main/java/net/croz/nrich/core/api/exception/ExceptionWithMessage.java

[exception-with-arguments-url]: ../nrich-core-api/src/main/java/net/croz/nrich/core/api/exception/ExceptionWithArguments.java
1 change: 1 addition & 0 deletions nrich-notification/build.gradle
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
description = "Provides a unified response format for data returned to clients"

dependencies {
api project(":nrich-core-api")
api project(":nrich-notification-api")

annotationProcessor "org.projectlombok:lombok"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,9 @@
package net.croz.nrich.notification.service;

import lombok.RequiredArgsConstructor;
import net.croz.nrich.core.api.exception.ExceptionWithArguments;
import net.croz.nrich.core.api.exception.ExceptionWithMessage;
import net.croz.nrich.core.api.exception.ExceptionWithMessageCode;
import net.croz.nrich.notification.api.model.AdditionalNotificationData;
import net.croz.nrich.notification.api.model.Notification;
import net.croz.nrich.notification.api.model.NotificationSeverity;
Expand Down Expand Up @@ -87,15 +90,15 @@ public ValidationFailureNotification createNotificationForValidationFailure(Cons
}

@Override
public Notification createNotificationForException(Throwable throwable, AdditionalNotificationData additionalNotificationData, Object... exceptionMessageArgumentList) {
public Notification createNotificationForException(Throwable throwable, AdditionalNotificationData additionalNotificationData) {
String typeName = throwable.getClass().getName();
String titleCode = String.format(NotificationConstants.PREFIX_MESSAGE_FORMAT, typeName, NotificationConstants.MESSAGE_TITLE_SUFFIX);

String title = notificationMessageResolverService.resolveMessage(toList(titleCode, NotificationConstants.ERROR_OCCURRED_MESSAGE_TITLE_CODE), NotificationConstants.EMPTY_MESSAGE);
String contentCode = String.format(NotificationConstants.PREFIX_MESSAGE_FORMAT, typeName, NotificationConstants.MESSAGE_CONTENT_SUFFIX);

String severityCode = String.format(NotificationConstants.PREFIX_MESSAGE_FORMAT, typeName, NotificationConstants.MESSAGE_SEVERITY_SUFFIX);
String content = resolveExceptionContent(throwable);

String content = notificationMessageResolverService.resolveMessage(toList(contentCode, NotificationConstants.ERROR_OCCURRED_DEFAULT_CODE), toList(exceptionMessageArgumentList), null);
NotificationSeverity severity = Optional.ofNullable(additionalNotificationData.getSeverity()).orElse(resolveExceptionSeverity(severityCode));
List<String> messageList = resolveMessageListFromNotificationData(additionalNotificationData.getMessageListDataMap());

Expand Down Expand Up @@ -159,8 +162,30 @@ private NotificationSeverity resolveExceptionSeverity(String messageCode) {
return NotificationSeverity.valueOf(severityValue);
}

private String resolveExceptionContent(Throwable throwable) {
if (throwable instanceof ExceptionWithMessage) {
return throwable.getMessage();
}

String contentCode;
if (throwable instanceof ExceptionWithMessageCode exceptionWithMessageCode) {
contentCode = exceptionWithMessageCode.getMessageCode();
}
else {
String typeName = throwable.getClass().getName();
contentCode = String.format(NotificationConstants.PREFIX_MESSAGE_FORMAT, typeName, NotificationConstants.MESSAGE_CONTENT_SUFFIX);
}

List<Object> argumentList = new ArrayList<>();
if (throwable instanceof ExceptionWithArguments exceptionWithArguments) {
argumentList.addAll(toList(exceptionWithArguments.getArgumentList()));
}

return notificationMessageResolverService.resolveMessage(toList(contentCode, NotificationConstants.ERROR_OCCURRED_DEFAULT_CODE), argumentList, null);
}

@SafeVarargs
private final <T> List<T> toList(T... codeList) {
private <T> List<T> toList(T... codeList) {
if (codeList == null) {
return Collections.emptyList();
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -55,8 +55,8 @@ public NotificationResponse responseWithValidationFailureNotification(Constraint
}

@Override
public NotificationResponse responseWithExceptionNotification(Throwable throwable, AdditionalNotificationData additionalNotificationData, Object... exceptionMessageArgumentList) {
Notification notification = notificationResolverService.createNotificationForException(throwable, additionalNotificationData, exceptionMessageArgumentList);
public NotificationResponse responseWithExceptionNotification(Throwable throwable, AdditionalNotificationData additionalNotificationData) {
Notification notification = notificationResolverService.createNotificationForException(throwable, additionalNotificationData);

return new NotificationResponse(notification);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,8 @@
import net.croz.nrich.notification.stub.NotificationResolverServiceTestException;
import net.croz.nrich.notification.stub.NotificationResolverServiceTestExceptionWithArguments;
import net.croz.nrich.notification.stub.NotificationResolverServiceTestExceptionWithCustomTitle;
import net.croz.nrich.notification.stub.NotificationResolverServiceTestExceptionWithMessage;
import net.croz.nrich.notification.stub.NotificationResolverServiceTestExceptionWithMessageCode;
import net.croz.nrich.notification.stub.NotificationResolverServiceTestRequest;
import net.croz.nrich.notification.stub.NotificationResolverServiceTestRequestWithCustomTitle;
import net.croz.nrich.notification.stub.NotificationResolverServiceTestWithoutMessageRequest;
Expand Down Expand Up @@ -157,10 +159,10 @@ void shouldReturnDefinedExceptionMessageAndSeverity() {
@Test
void shouldAddMessageArgumentsForDefinedExceptions() {
// given
Exception exception = new NotificationResolverServiceTestExceptionWithArguments("message");
Exception exception = new NotificationResolverServiceTestExceptionWithArguments("message", "1");

// when
Notification notification = defaultNotificationResolverService.createNotificationForException(exception, "1");
Notification notification = defaultNotificationResolverService.createNotificationForException(exception);

// then
assertThat(notification).isNotNull();
Expand Down Expand Up @@ -322,14 +324,42 @@ void shouldConvertConstraintViolationExceptionTooNotificationErrorResponse() {

@Test
void shouldNotFailWithNullArguments() {
// when
// given
AdditionalNotificationData notificationData = AdditionalNotificationData.builder().build();
Throwable thrown = catchThrowable(() -> defaultNotificationResolverService.createNotificationForException(new RuntimeException(), notificationData, (Object[]) null));
Exception exception = new NotificationResolverServiceTestExceptionWithArguments("", (Object[]) null);

// when
Throwable thrown = catchThrowable(() -> defaultNotificationResolverService.createNotificationForException(exception, notificationData));

// then
assertThat(thrown).isNull();
}

@Test
void shouldResolveNotificationContentFromProvidedMessageCode() {
// given
Exception exception = new NotificationResolverServiceTestExceptionWithMessageCode();

// when
Notification notification = defaultNotificationResolverService.createNotificationForException(exception);

// then
assertThat(notification.getContent()).isEqualTo("Content resolved from message code");
}

@Test
void shouldResolveNotificationContentFromProvidedMessage() {
// given
String messageContent = "Message content";
Exception exception = new NotificationResolverServiceTestExceptionWithMessage(messageContent);

// when
Notification notification = defaultNotificationResolverService.createNotificationForException(exception);

// then
assertThat(notification.getContent()).isEqualTo(messageContent);
}

private BindingResult validate(Object objectToValidate, Map<String, Object> valueMap) {
DataBinder binder = new DataBinder(objectToValidate);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,9 +18,10 @@
package net.croz.nrich.notification.stub;

import lombok.Getter;
import net.croz.nrich.core.api.exception.ExceptionWithArguments;

@Getter
public class NotificationResolverServiceTestExceptionWithArguments extends RuntimeException {
public class NotificationResolverServiceTestExceptionWithArguments extends RuntimeException implements ExceptionWithArguments {

private final transient Object[] argumentList;

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
/*
* Copyright 2020-2023 CROZ d.o.o, the original author or 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 net.croz.nrich.notification.stub;

import net.croz.nrich.core.api.exception.ExceptionWithMessage;

public class NotificationResolverServiceTestExceptionWithMessage extends RuntimeException implements ExceptionWithMessage {

public NotificationResolverServiceTestExceptionWithMessage(String message) {
super(message);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
/*
* Copyright 2020-2023 CROZ d.o.o, the original author or 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 net.croz.nrich.notification.stub;

import net.croz.nrich.core.api.exception.ExceptionWithMessageCode;

public class NotificationResolverServiceTestExceptionWithMessageCode extends RuntimeException implements ExceptionWithMessageCode {

@Override
public String getMessageCode() {
return "notification-with-custom-message-code.content";
}
}
20 changes: 19 additions & 1 deletion nrich-notification/src/test/resources/messages.properties
Original file line number Diff line number Diff line change
@@ -1,3 +1,20 @@
#
# Copyright 2020-2023 CROZ d.o.o, the original author or 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.
#
#

notification.success.title=Success
notification.success.default-message=Action has been executed
notification.validation-failed.title=Validation failed
Expand All @@ -20,9 +37,10 @@ net.croz.nrich.notification.stub.NotificationResolverServiceTestException.severi
net.croz.nrich.notification.stub.NotificationResolverServiceTestExceptionWithArguments.content=Error message with arguments: {0}

net.croz.nrich.notification.stub.NotificationResolverServiceTestExceptionWithCustomTitle.title=Custom error title

net.croz.nrich.notification.stub.NotificationResolverServiceTestRequestWithCustomTitle.title=Validation failure custom title

notification-with-custom-message-code.content=Content resolved from message code

upload.finished.content=Upload finished
upload.finished.with-title.content=Upload finished
upload.finished.with-title.title=Custom title
Expand Down
1 change: 0 additions & 1 deletion nrich-webmvc/build.gradle
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
description = "Provides @RestControllerAdvice for exception logging and notification resolving and additional serialization and locale features"

dependencies {
api project(":nrich-core-api")
api project(":nrich-logging-api")
api project(":nrich-notification-api")
api project(":nrich-webmvc-api")
Expand Down
Loading

0 comments on commit 2ffe052

Please sign in to comment.