Basic (user) main view controller class

When developing many Jmix based applications it might be convenient not to repeat yourself too often.

I did some development on Jmix applications with left and some with top navigation as well with similar basic behaviour. So I created a basic main view with user support extending the StandardMain view saving some duplicate code. This helps reducing templated code and reduce migration efforts.

A suggestion

import java.io.Serial;
import java.util.Objects;

import com.vaadin.flow.component.Component;
import com.vaadin.flow.component.avatar.Avatar;
import com.vaadin.flow.component.avatar.AvatarVariant;
import com.vaadin.flow.component.html.Div;
import com.vaadin.flow.component.html.Span;

import org.springframework.lang.NonNull;
import org.springframework.lang.Nullable;
import org.springframework.security.core.userdetails.UserDetails;

import io.jmix.core.Messages;
import io.jmix.core.usersubstitution.CurrentUserSubstitution;
import io.jmix.flowui.UiComponents;
import io.jmix.flowui.app.main.StandardMainView;

/**
 * Common implementation for different main view variants offering basic user menu support.
 *
 * @param <U>
 *            A user type.
 */
public class BasicMainView<U extends UserDetails> extends StandardMainView
{
    /**
     * Defines a default <i>Vaadin</i> route (path).
     */
    public static final String ROUTE_PATH = "";

    /**
     * Generated serialization identifier.
     */
    @Serial
    private static final long serialVersionUID = -4485091503059586394L;

    /**
     * Holds the constructor injected <i>Jmix</i> core messages component.
     */
    protected final Messages messages;

    /**
     * Holds the constructor injected <i>Jmix</i> core current-user-substitution component.
     */
    protected final CurrentUserSubstitution currentUserSubstitution;

    /**
     * Holds the constructor injected <i>Jmix</i> (flow) UI components component.
     */
    protected final UiComponents uiComponents;

    /**
     * Basic main view constructor injecting dependencies.
     *
     * @param someMessages
     *            A <i>Jmix</i> core messages component.
     * @param aCurrentUserSubstitution
     *            A <i>Jmix</i> core current user substitution component.
     * @param someUIComponents
     *            A <i>Jmix</i> (flow) UI components component.
     * @throws NullPointerException
     *            Will be thrown, if any argument is {@code null}.
     */
    protected BasicMainView(@NonNull final Messages someMessages,
                    @NonNull final CurrentUserSubstitution aCurrentUserSubstitution,
                    @NonNull final UiComponents someUIComponents)
    {
        super();

        Objects.requireNonNull(someMessages);
        Objects.requireNonNull(aCurrentUserSubstitution);
        Objects.requireNonNull(someUIComponents);

        messages = someMessages;
        currentUserSubstitution = aCurrentUserSubstitution;
        uiComponents = someUIComponents;
    }

    /**
     * <p>Renders some user specific details onto the user menu button.</p>
     * <p>This method is indented to be called by the application specific main view controller button renderer, e.g.:
     * </p>
     * <pre>
     *     {@literal @}Install(to = "userMenu", subject = "buttonRenderer")
     *     private Component userMenuButtonRenderer(final UserDetails someUserDetails) {
     *         return someUserDetails instanceof final User user ? createUserMenuButtonRenderer(user) : null;
     *     }
     * </pre>
     *
     * @param aUser
     *            A user.
     * @return button component or {@code null}
     */
    protected Component createUserMenuButtonRenderer(@Nullable final U aUser)
    {
        final Component result;
        if (aUser == null)
        {
            result = null;
        }
        else
        {
            final boolean substituted = isSubstituted(aUser);

            final String name = generateName(aUser);

            final Div content = uiComponents.create(Div.class);
            content.setClassName("user-menu-button-content");

            final Avatar avatar = createAvatar(name);
            if (!substituted)
            {
                final String avatarUrl = getUserAvatarUrl(aUser);
                if (avatarUrl != null)
                {
                    avatar.setImage(avatarUrl);
                }
            }

            final Span text = uiComponents.create(Span.class);
            text.setText(name);
            text.setClassName("user-menu-text");

            content.add(avatar, text);

            if (substituted)
            {
                final Span subtext = uiComponents.create(Span.class);
                subtext.setText(messages.getMessage("userMenu.substituted"));
                subtext.setClassName("user-menu-subtext");

                content.add(subtext);
            }

            result = content;
        }

        return result;
    }

    /**
     * <p>Renders some user specific details onto the user menu header.</p>
     * <p>This method is indented to be called by the application specific main view controller button renderer, e.g.:
     * </p>
     * <pre>
     *     {@literal @}Install(to = "userMenu", subject = "headerRenderer")
     *     private Component userMenuHeaderRenderer(final UserDetails someUserDetails) {
     *         return someUserDetails instanceof final User user ? createUserMenuHeaderRenderer(user) : null;
     *     }
     * </pre>
     *
     * @param aUser
     *            A user.
     * @return header content component or {@code null}
     */
    protected Component createUserMenuHeaderRenderer(@Nullable final U aUser)
    {
        final Component result;
        if (aUser == null)
        {
            result = null;
        }
        else
        {
            final Div content = uiComponents.create(Div.class);
            content.setClassName("user-menu-header-content");

            final String name = generateName(aUser);

            final boolean substituted = isSubstituted(aUser);

            final Avatar avatar = createAvatar(name);
            avatar.addThemeVariants(AvatarVariant.LUMO_LARGE);
            if (!substituted)
            {
                final String avatarUrl = getUserAvatarUrl(aUser);
                if (avatarUrl != null)
                {
                    avatar.setImage(avatarUrl);
                }
            }

            final Span text = uiComponents.create(Span.class);
            text.setText(name);
            text.setClassName("user-menu-text");

            content.add(avatar, text);

            if (Objects.equals(name, aUser.getUsername()))
            {
                text.addClassNames("user-menu-text-subtext");
            }
            else
            {
                final Span subtext = uiComponents.create(Span.class);
                subtext.setText(aUser.getUsername());
                subtext.setClassName("user-menu-subtext");

                content.add(subtext);
            }

            result = content;
        }

        return result;
    }

    /**
     * <p>Default implementation to return a custom user avatar URL for a given user or {@code null}.</p>
     * <p>Overwrite this method to return a user specific avatar URL.</p>
     *
     * @param aUser
     *            A user.
     * @return user avatar URL
     */
    @Nullable
    protected String getUserAvatarUrl(@Nullable final U aUser)
    {
        return null;
    }

    /**
     * Creates an avatar with the given name.
     *
     * @param aName
     *            A name.
     * @return avatar
     */
    @NonNull
    protected Avatar createAvatar(final String aName)
    {
        final Avatar avatar = uiComponents.create(Avatar.class);
        avatar.setName(aName);
        avatar.getElement().setAttribute("tabindex", "-1");
        avatar.setClassName("user-menu-avatar");

        return avatar;
    }

    /**
     * <p>Default implementation to return a user's name.</p>
     * <p>This method is intended to be overwritten by the application specific main view controller, e.g.:</p>
     * <pre>
     * {@literal @}Override
     * protected String generateName(final User aUser) {
     *     String userName = String.format("%s %s",
     *                     Strings.nullToEmpty(aUser.getFirstName()),
     *                     Strings.nullToEmpty(aUser.getLastName()))
     *                .trim();
     *
     *     return userName.isEmpty() ? super.generateName(aUser) : userName;
     * }
     * </pre>
     *
     * @param aUser
     *            A user.
     * @return name
     */
    @NonNull
    protected String generateName(@Nullable final U aUser)
    {
        return aUser == null ? "" : aUser.getUsername();
    }

    /**
     * Returns, whether the given user is substituted by comparing the {@code username} of the currently authenticated
     * on with the given user's {@code username}.
     *
     * @param aUser
     *            A user.
     * @return substituted
     */
    protected boolean isSubstituted(@Nullable final U aUser)
    {
        final UserDetails authenticatedUser = currentUserSubstitution.getAuthenticatedUser();

        return aUser != null && !Objects.equals(authenticatedUser.getUsername(), aUser.getUsername());
    }
}