UI error handling refactoring (#1106)

* refactored HawkbitUIErrorHandler to delegate error details extraction to external extractor beans
* refactored ui error handling, allowed ui error details extractors to return a list of error details
* added license headers, restructured package structure
* adapted javadocs
* fixed sonar findings
* fixed license header
* added tests for HawkbitUIErrorHandler
* refactored ConstraintViolationErrorExtractor, added test for extractors
* changed UI tests feature to Management UI
* fixed the parent/child error type resolution by ui error details extractor, added test

Signed-off-by: Bogdan Bondar <Bogdan.Bondar@bosch.io>
This commit is contained in:
Bondar Bogdan
2021-04-22 08:19:45 +02:00
committed by GitHub
parent cf67467fb5
commit 5bcaf3d99b
14 changed files with 840 additions and 154 deletions

View File

@@ -8,8 +8,8 @@
*/
package org.eclipse.hawkbit.ui;
import org.eclipse.hawkbit.ui.components.HawkbitUIErrorHandler;
import org.eclipse.hawkbit.ui.components.NotificationUnreadButton;
import org.eclipse.hawkbit.ui.error.ErrorView;
import org.eclipse.hawkbit.ui.menu.DashboardEvent.PostViewChangeEvent;
import org.eclipse.hawkbit.ui.menu.DashboardMenu;
import org.eclipse.hawkbit.ui.menu.DashboardMenuItem;
@@ -34,6 +34,7 @@ import com.vaadin.navigator.View;
import com.vaadin.navigator.ViewChangeListener;
import com.vaadin.navigator.ViewProvider;
import com.vaadin.server.ClientConnector.DetachListener;
import com.vaadin.server.ErrorHandler;
import com.vaadin.server.Responsive;
import com.vaadin.server.VaadinRequest;
import com.vaadin.spring.navigator.SpringViewProvider;
@@ -73,6 +74,7 @@ public abstract class AbstractHawkbitUI extends UI implements DetachListener {
private final SpringViewProvider viewProvider;
private final transient ApplicationContext context;
private final transient EventPushStrategy pushStrategy;
private final transient ErrorHandler uiErrorHandler;
private final transient HawkbitEntityEventListener entityEventsListener;
@@ -80,7 +82,7 @@ public abstract class AbstractHawkbitUI extends UI implements DetachListener {
final UIEventProvider eventProvider, final SpringViewProvider viewProvider,
final ApplicationContext context, final DashboardMenu dashboardMenu, final ErrorView errorview,
final NotificationUnreadButton notificationUnreadButton, final UiProperties uiProperties,
final VaadinMessageSource i18n) {
final VaadinMessageSource i18n, final ErrorHandler uiErrorHandler) {
this.pushStrategy = pushStrategy;
this.viewProvider = viewProvider;
this.context = context;
@@ -89,6 +91,7 @@ public abstract class AbstractHawkbitUI extends UI implements DetachListener {
this.notificationUnreadButton = notificationUnreadButton;
this.uiProperties = uiProperties;
this.i18n = i18n;
this.uiErrorHandler = uiErrorHandler;
this.entityEventsListener = new HawkbitEntityEventListener(eventBus, eventProvider, notificationUnreadButton);
}
@@ -184,7 +187,7 @@ public abstract class AbstractHawkbitUI extends UI implements DetachListener {
setNavigator(navigator);
if (UI.getCurrent().getErrorHandler() == null) {
UI.getCurrent().setErrorHandler(new HawkbitUIErrorHandler());
UI.getCurrent().setErrorHandler(uiErrorHandler);
}
LOG.debug("Current locale of the application is : {}", getLocale());

View File

@@ -8,7 +8,13 @@
*/
package org.eclipse.hawkbit.ui;
import java.util.List;
import org.eclipse.hawkbit.im.authentication.PermissionService;
import org.eclipse.hawkbit.ui.error.HawkbitUIErrorHandler;
import org.eclipse.hawkbit.ui.error.extractors.ConstraintViolationErrorExtractor;
import org.eclipse.hawkbit.ui.error.extractors.UiErrorDetailsExtractor;
import org.eclipse.hawkbit.ui.error.extractors.UploadErrorExtractor;
import org.eclipse.hawkbit.ui.utils.VaadinMessageSource;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
@@ -19,6 +25,7 @@ import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.PropertySource;
import org.vaadin.spring.servlet.Vaadin4SpringServlet;
import com.vaadin.server.ErrorHandler;
import com.vaadin.server.SystemMessagesProvider;
import com.vaadin.server.VaadinServlet;
@@ -76,6 +83,28 @@ public class MgmtUiConfiguration {
return new LocalizedSystemMessagesProvider(uiProperties, i18n);
}
/**
* UI Error handler bean.
*
* @return UI Error handler
*/
@Bean
@ConditionalOnMissingBean
ErrorHandler uiErrorHandler(final VaadinMessageSource i18n,
final List<UiErrorDetailsExtractor> uiErrorDetailsExtractor) {
return new HawkbitUIErrorHandler(i18n, uiErrorDetailsExtractor);
}
@Bean
UiErrorDetailsExtractor uploadErrorExtractor() {
return new UploadErrorExtractor();
}
@Bean
UiErrorDetailsExtractor constraintViolationErrorExtractor(final VaadinMessageSource i18n) {
return new ConstraintViolationErrorExtractor(i18n);
}
/**
* Vaadin4Spring servlet bean.
*

View File

@@ -1,146 +0,0 @@
/**
* Copyright (c) 2015 Bosch Software Innovations GmbH and others.
*
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the Eclipse Public License v1.0
* which accompanies this distribution, and is available at
* http://www.eclipse.org/legal/epl-v10.html
*/
package org.eclipse.hawkbit.ui.components;
import java.util.Optional;
import java.util.Set;
import java.util.stream.Collectors;
import javax.validation.ConstraintViolation;
import javax.validation.ConstraintViolationException;
import org.eclipse.hawkbit.ui.common.notification.ParallelNotification;
import org.eclipse.hawkbit.ui.utils.SPUIStyleDefinitions;
import org.eclipse.hawkbit.ui.utils.SpringContextHolder;
import org.eclipse.hawkbit.ui.utils.UINotification;
import org.eclipse.hawkbit.ui.utils.VaadinMessageSource;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.util.StringUtils;
import com.vaadin.icons.VaadinIcons;
import com.vaadin.server.ClientConnector.ConnectorErrorEvent;
import com.vaadin.server.DefaultErrorHandler;
import com.vaadin.server.ErrorEvent;
import com.vaadin.server.Page;
import com.vaadin.server.UploadException;
import com.vaadin.shared.Connector;
import com.vaadin.ui.Component;
import com.vaadin.ui.Notification;
import com.vaadin.ui.UI;
/**
* Default handler for Hawkbit UI.
*/
public class HawkbitUIErrorHandler extends DefaultErrorHandler {
private static final long serialVersionUID = 1L;
private static final Logger LOG = LoggerFactory.getLogger(HawkbitUIErrorHandler.class);
@Override
public void error(final ErrorEvent event) {
// filter upload exceptions
if (event.getThrowable() instanceof UploadException) {
return;
}
final Notification notification = buildNotification(getRootExceptionFrom(event));
if (event instanceof ConnectorErrorEvent) {
final Connector connector = ((ConnectorErrorEvent) event).getConnector();
if (connector instanceof UI) {
final UI uiInstance = (UI) connector;
uiInstance.access(() -> notification.show(uiInstance.getPage()));
return;
}
}
final Optional<Page> originError = getPageOriginError(event);
if (originError.isPresent()) {
notification.show(originError.get());
return;
}
notification.show(Page.getCurrent());
}
private static Throwable getRootExceptionFrom(final ErrorEvent event) {
return getRootCauseOf(event.getThrowable());
}
private static Throwable getRootCauseOf(final Throwable ex) {
if (ex.getCause() != null) {
return getRootCauseOf(ex.getCause());
}
return ex;
}
private static Optional<Page> getPageOriginError(final ErrorEvent event) {
final Component errorOrigin = findAbstractComponent(event);
if (errorOrigin != null && errorOrigin.getUI() != null) {
return Optional.ofNullable(errorOrigin.getUI().getPage());
}
return Optional.empty();
}
/**
* Method to build a notification based on an exception.
*
* @param ex
* the throwable
* @return a hawkbit error notification message
*/
protected ParallelNotification buildNotification(final Throwable ex) {
LOG.error("Error in UI: ", ex);
final String errorMessage = extractMessageFrom(ex);
final VaadinMessageSource i18n = SpringContextHolder.getInstance().getBean(VaadinMessageSource.class);
return buildErrorNotification(i18n.getMessage("caption.error"), errorMessage);
}
/**
* Method to build a error notification based on caption and description.
*
* @param caption
* Caption
* @param description
* Description
* @return a hawkbit error notification message
*/
protected static ParallelNotification buildErrorNotification(final String caption, final String description) {
return UINotification.buildNotification(SPUIStyleDefinitions.SP_NOTIFICATION_ERROR_MESSAGE_STYLE, caption,
description, VaadinIcons.EXCLAMATION_CIRCLE, true);
}
private static String extractMessageFrom(final Throwable ex) {
if (!(ex instanceof ConstraintViolationException)) {
if (!StringUtils.isEmpty(ex.getMessage())) {
return ex.getMessage();
}
return ex.getClass().getSimpleName();
}
final Set<ConstraintViolation<?>> violations = ((ConstraintViolationException) ex).getConstraintViolations();
if (violations == null) {
return ex.getClass().getSimpleName();
} else {
return violations.stream().map(violation -> violation.getPropertyPath() + " " + violation.getMessage())
.collect(Collectors.joining(System.lineSeparator()));
}
}
}

View File

@@ -6,7 +6,7 @@
* which accompanies this distribution, and is available at
* http://www.eclipse.org/legal/epl-v10.html
*/
package org.eclipse.hawkbit.ui;
package org.eclipse.hawkbit.ui.error;
import org.eclipse.hawkbit.ui.menu.DashboardMenu;
import org.eclipse.hawkbit.ui.menu.DashboardMenuItem;

View File

@@ -0,0 +1,151 @@
/**
* Copyright (c) 2021 Bosch.IO GmbH and others.
*
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the Eclipse Public License v1.0
* which accompanies this distribution, and is available at
* http://www.eclipse.org/legal/epl-v10.html
*/
package org.eclipse.hawkbit.ui.error;
import java.util.Collection;
import java.util.List;
import java.util.Optional;
import java.util.stream.Collectors;
import org.eclipse.hawkbit.ui.common.notification.ParallelNotification;
import org.eclipse.hawkbit.ui.error.extractors.UiErrorDetailsExtractor;
import org.eclipse.hawkbit.ui.utils.SPUIStyleDefinitions;
import org.eclipse.hawkbit.ui.utils.UINotification;
import org.eclipse.hawkbit.ui.utils.VaadinMessageSource;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.vaadin.icons.VaadinIcons;
import com.vaadin.server.ClientConnector.ConnectorErrorEvent;
import com.vaadin.server.DefaultErrorHandler;
import com.vaadin.server.ErrorEvent;
import com.vaadin.server.ErrorHandler;
import com.vaadin.server.Page;
import com.vaadin.shared.Connector;
import com.vaadin.ui.Component;
import com.vaadin.ui.Notification;
import com.vaadin.ui.UI;
/**
* Default handler for Hawkbit UI.
*/
public class HawkbitUIErrorHandler implements ErrorHandler {
private static final long serialVersionUID = 1L;
private static final Logger LOG = LoggerFactory.getLogger(HawkbitUIErrorHandler.class);
private final VaadinMessageSource i18n;
private final transient List<UiErrorDetailsExtractor> uiErrorDetailsExtractors;
/**
* Constructor for HawkbitUIErrorHandler.
*
* @param i18n
* Message source used for localization
* @param uiErrorDetailsExtractors
* ui error details extractors
*/
public HawkbitUIErrorHandler(final VaadinMessageSource i18n,
final List<UiErrorDetailsExtractor> uiErrorDetailsExtractors) {
this.i18n = i18n;
this.uiErrorDetailsExtractors = uiErrorDetailsExtractors;
}
@Override
public void error(final ErrorEvent event) {
final Page currentPage = getPageFrom(event);
final List<UiErrorDetails> errorDetails = extractErrorDetails(event);
if (errorDetails.isEmpty()) {
showGenericErrorNotification(currentPage, event);
return;
}
errorDetails.stream().filter(UiErrorDetails::isPresent)
.forEach(details -> showSpecificErrorNotification(currentPage, details));
}
/**
* Method to find the {@link Page} to show notification on.
*
* @param event
* error event
* @return current {@link Page} for error notification
*/
protected Page getPageFrom(final ErrorEvent event) {
if (event instanceof ConnectorErrorEvent) {
final Connector connector = ((ConnectorErrorEvent) event).getConnector();
if (connector instanceof UI) {
final UI uiInstance = (UI) connector;
return uiInstance.getPage();
}
}
final Optional<Page> errorOriginPage = getErrorOriginPage(event);
if (errorOriginPage.isPresent()) {
return errorOriginPage.get();
}
return Page.getCurrent();
}
private static Optional<Page> getErrorOriginPage(final ErrorEvent event) {
final Component errorOrigin = DefaultErrorHandler.findAbstractComponent(event);
if (errorOrigin != null && errorOrigin.getUI() != null) {
return Optional.ofNullable(errorOrigin.getUI().getPage());
}
return Optional.empty();
}
private List<UiErrorDetails> extractErrorDetails(final ErrorEvent event) {
return uiErrorDetailsExtractors.stream()
.map(extractor -> extractor.extractErrorDetailsFrom(event.getThrowable())).flatMap(Collection::stream)
.collect(Collectors.toList());
}
private void showGenericErrorNotification(final Page page, final ErrorEvent event) {
LOG.error("Unexpected Ui error occured", event.getThrowable());
final Notification notification = buildErrorNotification(i18n.getMessage("caption.error"),
i18n.getMessage("message.error"));
showErrorNotification(page, notification);
}
private void showSpecificErrorNotification(final Page page, final UiErrorDetails details) {
final Notification notification = buildErrorNotification(details.getCaption(), details.getDescription());
showErrorNotification(page, notification);
}
/**
* Method to build an error notification based on caption and description.
*
* @param caption
* notification caption
* @param description
* notification description
* @return a hawkbit error notification message
*/
protected static ParallelNotification buildErrorNotification(final String caption, final String description) {
return UINotification.buildNotification(SPUIStyleDefinitions.SP_NOTIFICATION_ERROR_MESSAGE_STYLE, caption,
description, VaadinIcons.EXCLAMATION_CIRCLE, true);
}
/**
* Method to show notification on the given page.
*
* @param page
* page to show notification on
* @param notification
* notification to show
*/
protected void showErrorNotification(final Page page, final Notification notification) {
page.getUI().access(() -> notification.show(page));
}
}

View File

@@ -0,0 +1,67 @@
/**
* Copyright (c) 2021 Bosch.IO GmbH and others.
*
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the Eclipse Public License v1.0
* which accompanies this distribution, and is available at
* http://www.eclipse.org/legal/epl-v10.html
*/
package org.eclipse.hawkbit.ui.error;
/**
* Details of UI errors for building the error notification.
*/
public final class UiErrorDetails {
private static final UiErrorDetails EMPTY = new UiErrorDetails();
private final String caption;
private final String description;
private UiErrorDetails() {
this(null, null);
}
private UiErrorDetails(final String caption, final String description) {
this.caption = caption;
this.description = description;
}
public String getCaption() {
return caption;
}
public String getDescription() {
return description;
}
/**
* Checks if error details are not empty.
*
* @return if error details are populated
*/
public boolean isPresent() {
return caption != null && description != null;
}
/**
* Creates empty error details that should be ignored by error handler.
*
* @return empty error details
*/
public static UiErrorDetails empty() {
return EMPTY;
}
/**
* Creates error details that should be processed by error handler.
*
* @param caption
* error details caption
* @param description
* error details description
* @return error details
*/
public static UiErrorDetails create(final String caption, final String description) {
return new UiErrorDetails(caption, description);
}
}

View File

@@ -0,0 +1,35 @@
/**
* Copyright (c) 2021 Bosch.IO GmbH and others.
*
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the Eclipse Public License v1.0
* which accompanies this distribution, and is available at
* http://www.eclipse.org/legal/epl-v10.html
*/
package org.eclipse.hawkbit.ui.error.extractors;
import java.util.Collections;
import java.util.List;
import java.util.Optional;
import org.eclipse.hawkbit.ui.error.UiErrorDetails;
/**
* Base class for single UI error details extractors.
*/
public abstract class AbstractSingleUiErrorDetailsExtractor implements UiErrorDetailsExtractor {
@Override
public List<UiErrorDetails> extractErrorDetailsFrom(final Throwable error) {
return findDetails(error).map(Collections::singletonList).orElseGet(Collections::emptyList);
}
/**
* Extracts single ui error details from given error.
*
* @param error
* error to extract details from
* @return ui error details if found
*/
protected abstract Optional<UiErrorDetails> findDetails(Throwable error);
}

View File

@@ -0,0 +1,67 @@
/**
* Copyright (c) 2021 Bosch.IO GmbH and others.
*
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the Eclipse Public License v1.0
* which accompanies this distribution, and is available at
* http://www.eclipse.org/legal/epl-v10.html
*/
package org.eclipse.hawkbit.ui.error.extractors;
import java.util.Optional;
import java.util.Set;
import java.util.stream.Collectors;
import javax.validation.ConstraintViolation;
import javax.validation.ConstraintViolationException;
import org.apache.commons.lang3.StringUtils;
import org.eclipse.hawkbit.ui.error.UiErrorDetails;
import org.eclipse.hawkbit.ui.utils.VaadinMessageSource;
import org.springframework.util.CollectionUtils;
/**
* UI error details extractor for {@link ConstraintViolationException}.
*/
public class ConstraintViolationErrorExtractor extends AbstractSingleUiErrorDetailsExtractor {
private final VaadinMessageSource i18n;
/**
* Constructor for ConstraintViolationErrorExtractor.
*
* @param i18n
* Message source used for localization
*/
public ConstraintViolationErrorExtractor(final VaadinMessageSource i18n) {
this.i18n = i18n;
}
@Override
protected Optional<UiErrorDetails> findDetails(final Throwable error) {
return findExceptionOf(error, ConstraintViolationException.class).map(ex -> {
final StringBuilder descriptionBuilder = new StringBuilder(getBasicDescription(ex, error));
getViolationsDescription(ex).ifPresent(violationsDescription -> descriptionBuilder.append(":")
.append(System.lineSeparator()).append(violationsDescription));
return UiErrorDetails.create(i18n.getMessage("caption.error"), descriptionBuilder.toString());
});
}
private static String getBasicDescription(final ConstraintViolationException ex, final Throwable error) {
return StringUtils.isEmpty(ex.getMessage()) ? error.getClass().getSimpleName() : ex.getMessage();
}
private static Optional<String> getViolationsDescription(final ConstraintViolationException ex) {
final Set<ConstraintViolation<?>> violations = ex.getConstraintViolations();
if (!CollectionUtils.isEmpty(violations)) {
return Optional.of(formatViolations(violations));
}
return Optional.empty();
}
private static String formatViolations(final Set<ConstraintViolation<?>> violations) {
return violations.stream().map(violation -> violation.getPropertyPath() + " " + violation.getMessage())
.collect(Collectors.joining(System.lineSeparator()));
}
}

View File

@@ -0,0 +1,51 @@
/**
* Copyright (c) 2021 Bosch.IO GmbH and others.
*
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the Eclipse Public License v1.0
* which accompanies this distribution, and is available at
* http://www.eclipse.org/legal/epl-v10.html
*/
package org.eclipse.hawkbit.ui.error.extractors;
import java.util.List;
import java.util.Optional;
import org.eclipse.hawkbit.ui.error.UiErrorDetails;
/**
* Base interface for extracting ui error details from given error.
*/
@FunctionalInterface
public interface UiErrorDetailsExtractor {
/**
* Extracts ui error details from given error.
*
* @param error
* error to extract details from
* @return ui error details
*/
List<UiErrorDetails> extractErrorDetailsFrom(final Throwable error);
/**
* Tries to find out if error matches the given exception type.
*
* @param error
* error to match
* @param exceptionType
* the type of exception to match
* @return casted error if matched
*/
default <T> Optional<T> findExceptionOf(final Throwable error, final Class<T> exceptionType) {
if (exceptionType.isAssignableFrom(error.getClass())) {
return Optional.of((T) error);
}
if (error.getCause() != null) {
return findExceptionOf(error.getCause(), exceptionType);
}
return Optional.empty();
}
}

View File

@@ -0,0 +1,27 @@
/**
* Copyright (c) 2021 Bosch.IO GmbH and others.
*
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the Eclipse Public License v1.0
* which accompanies this distribution, and is available at
* http://www.eclipse.org/legal/epl-v10.html
*/
package org.eclipse.hawkbit.ui.error.extractors;
import java.util.Optional;
import org.eclipse.hawkbit.ui.error.UiErrorDetails;
import com.vaadin.server.UploadException;
/**
* UI error details extractor for {@link UploadException}.
*/
public class UploadErrorExtractor extends AbstractSingleUiErrorDetailsExtractor {
@Override
protected Optional<UiErrorDetails> findDetails(final Throwable error) {
// UploadException is ignored
return findExceptionOf(error, UploadException.class).map(ex -> UiErrorDetails.empty());
}
}