package com.airlock.iam.admin.application.configuration.users;

import com.airlock.iam.admin.application.configuration.accountlink.AccountLinkManagementConfig;
import com.airlock.iam.admin.application.configuration.token.TokenControllerIds;
import com.airlock.iam.admin.application.configuration.usersearch.UserProfileItemSearchConfig;
import com.airlock.iam.admin.application.configuration.usersearch.filter.UserSearchFilter;
import com.airlock.iam.admin.domain.model.token.TokenController;
import com.airlock.iam.admin.domain.model.user.UserLockedVisibility;
import com.airlock.iam.base.misc.log.LogUtil;
import com.airlock.iam.common.api.application.configuration.validation.UsernameValidatorConfig;
import com.airlock.iam.common.api.domain.model.role.Role;
import com.airlock.iam.common.api.domain.model.store.user.UserStore;
import com.airlock.iam.common.api.domain.model.user.PersistentUser;
import com.airlock.iam.common.application.configuration.rememberme.JspRememberMeConfig;
import com.airlock.iam.common.application.configuration.rememberme.RememberMeConfig;
import com.airlock.iam.common.application.configuration.user.UserManagementExtensionConfig;
import com.airlock.iam.common.application.configuration.user.UserProfileValueProviderConfig;
import com.airlock.iam.common.application.configuration.userprofile.UserProfileItemConfig;
import com.airlock.iam.core.application.configuration.contextdata.StringContextDataItemNameConfig;
import com.airlock.iam.core.domain.model.user.UserIdentityGenerator;
import com.airlock.iam.elog.api.ELogger;
import com.airlock.iam.elog.api.ELoggerFactory;
import com.airlock.iam.plugin.api.annotation.config.ConfigurationValidator;
import com.airlock.iam.plugin.api.annotation.config.Groups;
import com.airlock.iam.plugin.api.annotation.config.Plugin;
import com.airlock.iam.plugin.api.annotation.license.PluginPropertyLicenseRestriction;
import com.airlock.iam.plugin.api.annotation.lifecycle.AfterConfigActivation;
import com.airlock.iam.plugin.api.annotation.property.BooleanProperty;
import com.airlock.iam.plugin.api.annotation.property.EnumProperty;
import com.airlock.iam.plugin.api.annotation.property.GroupProperty;
import com.airlock.iam.plugin.api.annotation.property.IntegerProperty;
import com.airlock.iam.plugin.api.annotation.property.PluginListProperty;
import com.airlock.iam.plugin.api.annotation.property.PluginProperty;
import com.airlock.iam.plugin.api.annotation.property.StringListProperty;
import com.airlock.iam.plugin.api.annotation.property.StringProperty;
import com.airlock.iam.plugin.api.config.ConfigurationContext;
import com.airlock.iam.plugin.api.config.ConfigurationEnvironment;
import com.airlock.iam.plugin.api.config.Environment;
import com.airlock.iam.plugin.api.config.PluginConfiguration;
import com.airlock.iam.plugin.api.exception.ConfigurationException;
import com.airlock.iam.plugin.api.exception.InitializeException;
import com.airlock.iam.plugin.api.exception.PropertyConfigurationException;
import com.airlock.iam.plugin.api.exception.PropertyConstraintViolationException;
import com.airlock.iam.plugin.api.valuereader.BooleanPropertyValueReader;
import com.airlock.iam.plugin.api.valuereader.PluginListPropertyValueReader;
import com.airlock.iam.plugin.api.valuereader.PluginPropertyValueReader;
import com.airlock.iam.plugin.api.valuereader.StringListPropertyValueReader;
import com.airlock.iam.plugin.api.valuereader.StringPropertyValueReader;
import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.HashSet;
import java.util.List;
import java.util.Objects;
import java.util.Optional;
import java.util.Set;
import java.util.stream.Collectors;
import org.apache.commons.lang3.StringUtils;

@Plugin(description = "Configuration of users, i.e. authentication, authorization and management of users.", displayName = "Users Configuration")
@Groups(
   groupSequence = {
         "Basic Settings",
         "User List/Search page",
         "User Details Page - General",
         "User Details Page - Authentication Tokens (Credentials)",
         "User Details Page - User Management Extensions",
         "Advanced Settings"
   },
   openGroups = {"Basic Settings", "User Details Page - Authentication Tokens (Credentials)"}
)
public class UsersConfiguration {
   private static final ELogger LOG = ELoggerFactory.getLogger(UsersConfiguration.class);
   static final String USERS_CONFIGURATION_DISPLAY_NAME = "Users Configuration";
   static final String SEARCH_SETTINGS = "User List/Search page";
   public static final String DETAIL_SETTINGS = "User Details Page - General";
   static final String USER_TOKENS_SETTINGS = "User Details Page - Authentication Tokens (Credentials)";
   static final String USER_MANAGEMENT_EXTENSIONS_SETTINGS = "User Details Page - User Management Extensions";
   @PluginListProperty(
      description = "List of authentication tokens that can be managed in the Adminapp.",
      optional = true,
      minElements = 1,
      displayName = "Authentication Tokens (Credentials)"
   )
   @GroupProperty(groupIdentifier = "User Details Page - Authentication Tokens (Credentials)")
   private List<TokenController> authenticationTokens;
   public static final String AUTHENTICATION_TOKENS_PROPERTY = "authenticationTokens";
   @BooleanProperty(
      description = "If this property is enabled, user data (profile items, roles and validity) are editable, new users can be inserted, and users can be deleted.",
      optional = true,
      defaultValue = "true",
      displayName = "Users Are Editable"
   )
   @GroupProperty(groupIdentifier = "Basic Settings")
   private boolean editable;
   private static final String EDITABLE_PROPERTY = "editable";
   public static final String USERS_ARE_EDITABLE_DISPLAY_NAME = "Users Are Editable";
   @StringListProperty(
      description = "<p>Set of roles assignable to users.</p>\n<p>Translations for the roles displayed in the Adminapp user management UI can be defined using the\nAdminapp translation keys <tt>roles.user.labels.[rolename]</tt>.</p>\n<p>This setting can be overwritten by \"Admin Role Specific Settings\".</p>",
      exampleElements = {"employee", "customer"},
      optional = true
   )
   @GroupProperty(groupIdentifier = "User Details Page - General")
   private List<String> availableUserRoles;
   @BooleanProperty(
      description = "If this property is enabled, a section with the user valid status, valid-from and valid-to dates (if information is available) is displayed. On the user edit page, the valid-from and valid-to dates can be edited.",
      optional = true,
      defaultValue = "false"
   )
   @GroupProperty(groupIdentifier = "User Details Page - General")
   private boolean showUserValidSection;
   @EnumProperty(
      description = "<p>Defines the visibility of the user-locked section on the user details page:<ul> <li>SHOW: show all attributes (the default)</li> <li>RESTRICTED: show some attributes suitable for users in Active Directory (no lock reason or date; unlock button but no lock button)</li> <li>HIDE: do not show the user locked section</li></ul></p>",
      optional = true,
      defaultValue = "SHOW"
   )
   @GroupProperty(groupIdentifier = "User Details Page - General")
   private UserLockedVisibility userLockedSection;
   @StringProperty(
      description = "Defines the name of the context data property with the users correspondence language. It is used when rendering letters. This property is optional (if it is not set, no language information is available for the renderer plugins.)",
      suggestedValues = "language",
      optional = true
   )
   @GroupProperty(groupIdentifier = "Advanced Settings")
   private String languageContextKey;
   @IntegerProperty(
      description = "The maximum number of users to list per page.",
      displayName = "Max Users To List",
      optional = true,
      defaultValue = 50,
      minValue = 1,
      exampleValues = {25, 50, 100}
   )
   @GroupProperty(groupIdentifier = "User List/Search page")
   private int maxUsersToList;
   public static final String MAX_USERS_TO_LIST_PROPERTY = "maxUsersToList";
   public static final String MAX_USERS_TO_LIST_DISPLAY_NAME = "Max Users To List";
   @PluginProperty(
      description = "Defines the context-data field that the user search uses instead of the actual 'username'. It has the following effects:<ul><li>The user search never searches in the default 'username' field, but in the configured field instead.</li><li>This applies also if the 'Only search username' checkbox is selected.</li><li>The 'username' column in the search result list displays the value of this field.</li><li>The title of the user detail pages uses the value of this field.</li></ul>Only fields that are set on all users (e.g. login alias) should be used. Users without a value in this field might not appear in search results or will be listed with empty usernames.",
      optional = true,
      displayName = "Username Search Field"
   )
   @GroupProperty(groupIdentifier = "User List/Search page")
   private StringContextDataItemNameConfig usernameSearchField;
   private static final String USERNAME_SEARCH_FIELD_PROPERTY = "usernameSearchField";
   private static final String USERNAME_SEARCH_FIELD_DISPLAY_NAME = "Username Search Field";
   @StringProperty(
      description = "Defines the column in the user list to sort by default. Use the property name (e.g. \"givenname\") to refer to a context data column. If left empty, the default sort is unspecified.",
      displayName = "Default Sort Column",
      optional = true,
      suggestedValues = {"username", "locked", "lat_succ_login"}
   )
   @GroupProperty(groupIdentifier = "User List/Search page")
   private String defaultSortColumn;
   private static final String[] DEFAULT_SORT_COLUMN_SUGGESTED_VALUES = new String[]{"username", "locked", "lat_succ_login"};
   private static final String DEFAULT_SORT_COLUMN_PROPERTY = "defaultSortColumn";
   private static final String DEFAULT_SORT_COLUMN_DISPLAY_NAME = "Default Sort Column";
   @BooleanProperty(
      description = "If enabled (and a \"Default Sort Column\" is set), the default user sort is ascending, otherwise descending.",
      optional = true,
      defaultValue = "true"
   )
   @GroupProperty(groupIdentifier = "User List/Search page")
   private boolean sortAscendingByDefault;
   @PluginListProperty(
      description = "The property names and labels of context data to be displayed on the user list page. For example, this could be the first-, lastname, and the assigned authentication method of the user.<p>The data for the columns is taken from the context data container of the displayed users. <b>The configuration of the used user persister must include the context data properties referenced here.</b></p><p>The colums are displayed in addition to the following columns:<ul> <li>username</li> <li>locked state</li> <li>latest login date</li></ul></p>",
      optional = true
   )
   @GroupProperty(groupIdentifier = "User List/Search page")
   private List<UserProfileItemConfig> columnsInUserList;
   private static final String COLUMNS_IN_USER_LIST_PROPERTY = "columnsInUserList";
   private static final String COLUMNS_IN_USER_LIST_DISPLAY_NAME = "Columns In User List";
   static final String COLUMNS_IN_USER_LIST_DESCRIPTION = "The property names and labels of context data to be displayed on the user list page. For example, this could be the first-, lastname, and the assigned authentication method of the user.<p>The data for the columns is taken from the context data container of the displayed users. <b>The configuration of the used user persister must include the context data properties referenced here.</b></p><p>The colums are displayed in addition to the following columns:<ul> <li>username</li> <li>locked state</li> <li>latest login date</li></ul></p>";
   @PluginListProperty(
      description = "The items referring to \"Columns In User List\" that can be included in the search on the user list page. If no properties are configured, the search will not filter on context data items. If the referred item is not configured in the \"Columns In User List\", it will be ignored.  The configured properties are searchable in through the search functionality.<p><b>Note:</b> Currently only string and list items are supported by the search. All other items will be ignored.</p>",
      displayName = "User Profile Items For Search",
      optional = true
   )
   @GroupProperty(groupIdentifier = "User List/Search page")
   private List<UserProfileItemSearchConfig> userSearchProfileItems;
   private static final String SEARCH_PROFILE_ITEMS_PROPERTY = "userSearchProfileItems";
   private static final String SEARCH_PROFILE_ITEMS_DISPLAY_NAME = "User Profile Items For Search";
   @PluginListProperty(
      description = "The property names and labels of context data to be displayed on the user detail page.<p>The data is taken from the context data container of the selected user. <b>The configuration of the used user persister must include the context data properties referenced here.</b> </p><br /><b>Note: </b> The username is always displayed and can therefore not be configured here. Whether it is editable can be configured in the \"Access Control\" configuration in the \"Admin Tool\" root node.",
      optional = true
   )
   @GroupProperty(groupIdentifier = "User Details Page - General")
   private List<UserProfileItemConfig> rowsOnUserDetailPage;
   public static final String ROWS_ON_USER_DETAIL_PAGE_DISPLAY_NAME = "Rows On User Detail Page";
   @PluginProperty(
      description = "Defines how to load and store users in the Adminapp.\n<p>This setting can be overwritten by \"Admin Role Specific Settings\".</p>",
      optional = false,
      displayName = "User Data Source"
   )
   @GroupProperty(groupIdentifier = "Basic Settings")
   private UserRepositoryConfiguration userDataSource;
   private static final String USER_DATA_SOURCE_PROPERTY = "userDataSource";
   static final String USER_DATA_SOURCE_DISPLAY_NAME = "User Data Source";
   @PluginListProperty(
      description = "This list of admin role specific settings allows to overwrite certain settings for administrators with a specific set of roles. More details on what behaviour can be overwritten can be found within the plugin itself. The first Admin Role Specific Setting that matches the administrator's roles based on the \"Role Specific Settings Selection\" is used.",
      optional = true
   )
   @GroupProperty(groupIdentifier = "Advanced Settings")
   private List<AdminRoleSpecificSetting> adminRoleSpecificSettings;
   private static final String ADMIN_ROLE_SPECIFIC_SETTINGS_PROPERTY = "adminRoleSpecificSettings";
   @PluginProperty(
      description = "The strategy to select the role specific settings for an administrator. The first Admin Role Specific Settings that match the administrator's roles are used. If there is no matching entry, no role-specific settings are used.",
      displayName = "Role Specific Settings Selection",
      optional = true,
      defaultValue = AnyRequiredRoleMatcher.class
   )
   @GroupProperty(groupIdentifier = "Advanced Settings")
   private RolebasedConfigSelectionStrategy roleSpecificSettingsSelection;
   static final String ROLE_SPECIFIC_SELECTION_STRATEGY_DISPLAY_NAME = "Role Specific Settings Selection";
   @PluginProperty(
      description = "Configures the account link management. If account linking is configured and a user has account links, the account links tab, including the users account links, is displayed on the user detail page.",
      optional = true
   )
   @GroupProperty(groupIdentifier = "Advanced Settings")
   @PluginPropertyLicenseRestriction(moduleAffiliation = "OAuthAccountLinking")
   private AccountLinkManagementConfig accountLinkManagementConfig;
   @PluginProperty(description = "Configures the behavior of user locking.", optional = false)
   @GroupProperty(groupIdentifier = "User Details Page - General")
   private LockingSettings lockingSettings;
   @BooleanProperty(
      description = "If enabled, the credential migration section is displayed on the user details page. This section contains a choice of authentication methods to be assigned to the user after migration.",
      optional = true,
      defaultValue = "false"
   )
   @GroupProperty(groupIdentifier = "User Details Page - Authentication Tokens (Credentials)")
   @PluginPropertyLicenseRestriction(moduleAffiliation = "TokenSelfService")
   private boolean showMigrationSection;
   @BooleanProperty(
      description = "If enabled, multiple methods can be selected to which the user can migrate. On the migration hint page the users will be given a choice to which methods they would like to migrate. This is only functional if \"Show Migration Section\" above is also activated. Also remember to enable support for multiple next authentication methods on the migration hint page settings for the login app.",
      optional = true,
      defaultValue = "false"
   )
   @GroupProperty(groupIdentifier = "User Details Page - Authentication Tokens (Credentials)")
   @PluginPropertyLicenseRestriction(moduleAffiliation = "TokenSelfService")
   private boolean enableMultipleNextAuthMethods;
   @PluginListProperty(
      description = "<p>User Management Extensions are added as a tab in the User Management of the Adminapp.</p>\n<p>For someone to see the User Management Extension, they need to have the corresponding access rights.\nThis has to be set in the Adminapp Access Control.</p>\n<p>Please consult the official Airlock IAM documentation for more information</p>",
      optional = true
   )
   @GroupProperty(groupIdentifier = "Advanced Settings")
   private List<UserManagementExtensionConfig> userManagementExtensions;
   private static final String USER_MANAGEMENT_EXTENSIONS_PROPERTY = "userManagementExtensions";
   @StringListProperty(
      description = "This setting is used if more than one active authentication method must be managed in the Adminapp. It defines a list of user context properties. The properties are used to store the additional active authentication methods (and to select the corresponding translation for the label on the user details page). <br/>Make sure that the corresponding context properties are supported (i.e. configured in) by the configured user persister.",
      optional = true,
      exampleElements = {"mobileAppAuthMethod", "offlineToolAuthMethod", "radiusAuthMethod"}
   )
   @GroupProperty(groupIdentifier = "Advanced Settings")
   private List<String> additionalAuthMethodProperties;
   private static final String ADDITIONAL_AUTH_METHOD_PROPERTIES_PROPERTY = "additionalAuthMethodProperties";
   @BooleanProperty(
      description = "<p>If enabled, some user properties may be changed for users simultaneously (bulk change). In this release, the following properties can be set as bulk change:<ul> <li>Next authentication method (token migration) and migration date.</li></ul></p>Whether a property is available for bulk changes or not depends on the corresponding settings of the user details page (e.g. if the credential migration section is enabled on the user details page, it is also visible on the bulk changes page).<p></p><p>The bulk change page is accessible from the user list page.</p>",
      optional = true,
      defaultValue = "false"
   )
   @GroupProperty(groupIdentifier = "Advanced Settings")
   private boolean allowBulkChanges;
   @PluginProperty(
      description = "<p>If configured, this plugin is used to generate the ID (username) of new users. The \"Create new user\" dialogue in the Adminapp will not contain a username input field. REST requests to create a new user (<tt>POST /&lt;adminapp-uri&gt;/rest/users</tt>) will use this generator to create a user ID (instead of the default UUID generator)</p>",
      displayName = "User Identity Generator",
      notCombinableWith = "usernamePrefill",
      optional = true
   )
   @GroupProperty(groupIdentifier = "Advanced Settings")
   private UserIdentityGenerator userIdentityGenerator;
   private static final String USER_IDENTITY_GENERATOR_DISPLAY_NAME = "User Identity Generator";
   private static final String USER_IDENTITY_GENERATOR_PROPERTY_NAME = "userIdentityGenerator";
   @PluginProperty(
      description = "If configured, the username field on the user create and username change dialog is prefilled with the provided value. This feature can be used to suggest administrators on possible usernames or to prefill a common username prefix.",
      notCombinableWith = "userIdentityGenerator",
      optional = true
   )
   @GroupProperty(groupIdentifier = "Advanced Settings")
   private UserProfileValueProviderConfig usernamePrefill;
   private static final String USERNAME_PREFILL_PROPERTY_NAME = "usernamePrefill";
   @PluginProperty(
      description = "If configured, validates the username of new users before they are created. If the username is invalid, creation of this user fails with a corresponding validation error.<p>Be aware that independent of this validator, IAM enforces two default username validations on provided usernames:<ul><li>Uniqueness Validation - the provided username must not already exist on the database</li><li>Minimum Length Validation - the provided username must be at least 2 characters long</li></ul><p>Note that these validators (configured and default) are only applied for provided usernames and do not validate generated ones, i.e. when configuring a User Identity Generator.</p>",
      optional = true
   )
   @GroupProperty(groupIdentifier = "Advanced Settings")
   private UsernameValidatorConfig usernameValidator;
   @PluginListProperty(
      description = "<p>Some settings may be defined depending on a user's group membership (e.g. if the user is readable or not).</p><p>This list defines group-specific settings:<ul> <li>If the group's condition is met by the user, the corresponding settings are used for this user.</li> <li>It is processed in order of definition, i.e. the first matched group condition determines the settings used.</li> <li>If no group condition is met, the global user settings are used (= the ones just above).</ul></p>",
      optional = true
   )
   @GroupProperty(groupIdentifier = "Advanced Settings")
   private List<UserGroupConfiguration> groupSettings;
   @BooleanProperty(
      description = "Sets the default strategy for searching users. If enabled, searching is case-sensitive. Otherwise, searching is case-insensitive. The search strategy can always be altered by the corresponding checkbox on the user search page.",
      optional = true,
      displayName = "Case-Sensitive Search As Default",
      defaultValue = "true"
   )
   @GroupProperty(groupIdentifier = "User List/Search page")
   private boolean caseSensitiveSearchAsDefault;
   @BooleanProperty(
      description = "Sets the default strategy for searching users. If enabled, the search term must exactly match the username or an attribute to be found (equals-matching). Otherwise, the search term must be contained within the data (contains-matching). The search strategy can always be altered by the corresponding checkbox on the user search page.",
      optional = true,
      defaultValue = "false"
   )
   @GroupProperty(groupIdentifier = "User List/Search page")
   private boolean onlySearchWordsAsDefault;
   @BooleanProperty(
      description = "Sets the default strategy for searching users by their username. If enabled, the default search matches the term against the username (or the field configured as \"Username Search Field\", respectively). Otherwise, search is only performed on the configured \"User Profile Items For Search\". The search strategy can always be altered by the corresponding checkbox on the user search page.",
      optional = true,
      defaultValue = "true"
   )
   @GroupProperty(groupIdentifier = "User List/Search page")
   private boolean searchInUsernameAsDefault;
   @PluginListProperty(
      description = "Advanced search filters allow for extended search options. Please note that some filters can only be applied if a User Store with the required capability is configured.",
      optional = true
   )
   @GroupProperty(groupIdentifier = "User List/Search page")
   private List<UserSearchFilter> advancedSearchFilters;
   @BooleanProperty(
      description = "When enabled, the last user search is automatically performed again when navigating back to the user search page. When disabled, the last search term is still retained but the search must be triggered manually by the administrator. Disabling can be helpful in cases where searches can take a lot of time.",
      optional = true,
      defaultValue = "true"
   )
   @GroupProperty(groupIdentifier = "User List/Search page")
   private boolean automaticallyPerformLastSearch;
   @PluginProperty(
      description = "Configures the download of user data. If the download is not configured, it is not available in the Adminapp.",
      optional = true
   )
   @GroupProperty(groupIdentifier = "User List/Search page")
   private UsersExportConfig export;
   @BooleanProperty(
      description = "<p>If configured, the administrator will be able to delete all persisted Remember-Me tokens for a user on the user overview page.</p>",
      optional = true,
      defaultValue = "false",
      displayName = "Remember-Me Management Enabled"
   )
   @GroupProperty(groupIdentifier = "Advanced Settings")
   private boolean rememberMeManagementEnabled;
   private static final String REMEMBER_ME_MANAGEMENT_ENABLED_PROPERTY = "rememberMeManagementEnabled";
   @PluginProperty(
      description = "<p>If configured, the administrator will be able to delete all persisted Remember-Me tokens for a user on the user overview page.</p><p>If there are still users with legacy Remember-Me tokens from the JSP Loginapp in earlier IAM versions, also configure the JSP Loginapp Remember-Me Settings.</p>",
      optional = true,
      displayName = "Remember-Me Settings"
   )
   @GroupProperty(groupIdentifier = "Advanced Settings")
   private RememberMeConfig rememberMeSettings;
   private static final String REMEMBER_ME_SETTINGS_DISPLAY_NAME = "Remember-Me Settings";
   private static final String REMEMBER_ME_SETTINGS_PROPERTY = "rememberMeSettings";
   @PluginProperty(
      description = "These settings are only needed if there are still users with legacy Remember-Me tokens from the JSP Loginapp in earlier IAM versions. It allows an administrator to also see and delete the legacy Remember-Me tokens. Once all those tokens have been replaced or have expired, this setting can be removed.",
      optional = true,
      displayName = "JSP Loginapp Remember-Me Settings"
   )
   @GroupProperty(groupIdentifier = "Advanced Settings")
   private JspRememberMeConfig jspLoginappRememberMeSettings;
   private static final String JSP_LOGINAPP_REMEMBER_ME_SETTINGS_DISPLAY_NAME = "JSP Loginapp Remember-Me Settings";
   @PluginListProperty(
      description = "List of authorization servers where the users have OAuth 2.0 Tokens. This can for example be used to delete tokens on password changes by administrators.",
      displayName = "OAuth 2.0 Authorization Servers",
      optional = true,
      minElements = 1
   )
   @GroupProperty(groupIdentifier = "Advanced Settings")
   private List<UsersOAuth2AuthorizationServerConfig> oauth2AuthorizationServers;
   @BooleanProperty(
      description = "If enabled, administrators cannot delete users who have the same username as themselves. This applies regardless of where the admin or user\n are stored and prevents accidental self-deletion.\n<p>This can be useful if admin users are stored in the regular user repository.</p>\n",
      defaultValue = "false",
      optional = true
   )
   @GroupProperty(groupIdentifier = "Advanced Settings")
   private boolean adminCannotDeleteUserWithSameName;
   private RoleSpecificConfigSelector roleSpecificConfigSelector;

   public UsersConfiguration(
      List<TokenController> authenticationTokens,
      boolean editable,
      List<String> availableUserRoles,
      boolean showUserValidSection,
      UserLockedVisibility userLockedSection,
      String languageContextKey,
      int maxUsersToList,
      StringContextDataItemNameConfig usernameSearchField,
      String defaultSortColumn,
      boolean sortAscendingByDefault,
      List<UserProfileItemConfig> columnsInUserList,
      List<UserProfileItemSearchConfig> userSearchProfileItems,
      List<UserProfileItemConfig> rowsOnUserDetailPage,
      UserRepositoryConfiguration userDataSource,
      List<AdminRoleSpecificSetting> adminRoleSpecificSettings,
      RolebasedConfigSelectionStrategy roleSpecificSettingsSelection,
      AccountLinkManagementConfig accountLinkManagementConfig,
      LockingSettings lockingSettings,
      boolean showMigrationSection,
      boolean enableMultipleNextAuthMethods,
      List<UserManagementExtensionConfig> userManagementExtensions,
      List<String> additionalAuthMethodProperties,
      boolean allowBulkChanges,
      UserIdentityGenerator userIdentityGenerator,
      UserProfileValueProviderConfig usernamePrefill,
      UsernameValidatorConfig usernameValidator,
      List<UserGroupConfiguration> groupSettings,
      boolean caseSensitiveSearchAsDefault,
      boolean onlySearchWordsAsDefault,
      boolean searchInUsernameAsDefault,
      List<UserSearchFilter> advancedSearchFilters,
      boolean automaticallyPerformLastSearch,
      UsersExportConfig export,
      boolean rememberMeManagementEnabled,
      RememberMeConfig rememberMeSettings,
      JspRememberMeConfig jspLoginappRememberMeSettings,
      List<UsersOAuth2AuthorizationServerConfig> oauth2AuthorizationServers,
      boolean adminCannotDeleteUserWithSameName
   ) {
      this.authenticationTokens = authenticationTokens;
      this.editable = editable;
      this.availableUserRoles = availableUserRoles;
      this.showUserValidSection = showUserValidSection;
      this.userLockedSection = userLockedSection;
      this.languageContextKey = languageContextKey;
      this.maxUsersToList = maxUsersToList;
      this.usernameSearchField = usernameSearchField;
      this.defaultSortColumn = defaultSortColumn;
      this.sortAscendingByDefault = sortAscendingByDefault;
      this.columnsInUserList = columnsInUserList;
      this.userSearchProfileItems = userSearchProfileItems;
      this.rowsOnUserDetailPage = rowsOnUserDetailPage;
      this.userDataSource = userDataSource;
      this.adminRoleSpecificSettings = adminRoleSpecificSettings;
      this.roleSpecificSettingsSelection = roleSpecificSettingsSelection;
      this.accountLinkManagementConfig = accountLinkManagementConfig;
      this.lockingSettings = lockingSettings;
      this.showMigrationSection = showMigrationSection;
      this.enableMultipleNextAuthMethods = enableMultipleNextAuthMethods;
      this.userManagementExtensions = userManagementExtensions;
      this.additionalAuthMethodProperties = additionalAuthMethodProperties;
      this.allowBulkChanges = allowBulkChanges;
      this.userIdentityGenerator = userIdentityGenerator;
      this.usernamePrefill = usernamePrefill;
      this.usernameValidator = usernameValidator;
      this.groupSettings = groupSettings;
      this.caseSensitiveSearchAsDefault = caseSensitiveSearchAsDefault;
      this.onlySearchWordsAsDefault = onlySearchWordsAsDefault;
      this.searchInUsernameAsDefault = searchInUsernameAsDefault;
      this.advancedSearchFilters = advancedSearchFilters;
      this.automaticallyPerformLastSearch = automaticallyPerformLastSearch;
      this.export = export;
      this.rememberMeManagementEnabled = rememberMeManagementEnabled;
      this.rememberMeSettings = rememberMeSettings;
      this.jspLoginappRememberMeSettings = jspLoginappRememberMeSettings;
      this.oauth2AuthorizationServers = oauth2AuthorizationServers;
      this.adminCannotDeleteUserWithSameName = adminCannotDeleteUserWithSameName;
   }

   @AfterConfigActivation
   void initializeRoleSpecificConfigSelector() {
      this.roleSpecificConfigSelector = new RoleSpecificConfigSelector(
         this.adminRoleSpecificSettings, this.userDataSource, this.availableUserRoles, this.roleSpecificSettingsSelection
      );
   }

   @ConfigurationValidator
   public static void assureConfigConstraints(
      PluginConfiguration config, ConfigurationEnvironment env, ConfigurationContext context, Environment validationEnvironment
   ) throws ConfigurationException {
      checkAdditionalAuthMethodPropertiesUniqueness(config, env, context);
      checkUserRepositoryCapabilities(config, env, context);
      checkThatARoleBelongsOnlyToOneUserPersister(config, env, context);
      checkThatConfiguredDefaultSortColumnExistsIfConfigured(config, env, context);
      checkUniqueGenericTokenControllerIds(config, env, context);
      checkUserManagementIdUnique(config, env, context);
      checkSearchItemsInListColumns(config, env, context);
      checkRememberMeConfigMandatory(config, env, context);
   }

   private static void checkUniqueGenericTokenControllerIds(PluginConfiguration config, ConfigurationEnvironment env, ConfigurationContext context) throws ConfigurationException {
      List<PluginConfiguration> tokenControllers = PluginListPropertyValueReader.effectivePluginListPropertyValue(config, "authenticationTokens", env, context);
      Optional<String> firstConflictingId = TokenControllerIds.getInstance().getFirstConflictingId(tokenControllers, env, context);
      if (firstConflictingId.isPresent()) {
         throw new PropertyConstraintViolationException(
            "authenticationTokens",
            config,
            env,
            context,
            String.format("ID %s is used by multiple authentication token controllers.", firstConflictingId.get())
         );
      }
   }

   private static void checkAdditionalAuthMethodPropertiesUniqueness(PluginConfiguration config, ConfigurationEnvironment env, ConfigurationContext context) throws PropertyConfigurationException {
      List<String> authMethodProperties = StringListPropertyValueReader.effectiveStringListConfigValue(config, "additionalAuthMethodProperties", env, context);
      Set<String> props = new HashSet<>();

      for (String prop : authMethodProperties) {
         if (!props.add(prop)) {
            throw new PropertyConstraintViolationException(
               "additionalAuthMethodProperties",
               config,
               env,
               context,
               "The context property " + LogUtil.quote(prop) + " is listed multiple times. Each context property must be unique in this list."
            );
         }
      }
   }

   private static void checkUserRepositoryCapabilities(PluginConfiguration config, ConfigurationEnvironment env, ConfigurationContext context) throws ConfigurationException {
      try {
         if (!supportsInsertionAndDeletion(PluginPropertyValueReader.effectivePluginPropertyValue(config, "userDataSource", env, context), env, context)
            && BooleanPropertyValueReader.effectiveBooleanPropertyValue(config, "editable", env, context)) {
            throw new PropertyConstraintViolationException(
               "editable",
               config,
               env,
               context,
               "Users may be inserted and deleted (see property "
                  + LogUtil.quote("editable")
                  + ") but the configured user repository plugin (see \"User Data Source\") does not allow that."
            );
         }
      } catch (InitializeException e) {
         throw new ConfigurationException(config, env, context, "Cannot validate configuration: " + e.getMessage(), e);
      }
   }

   private static boolean supportsInsertionAndDeletion(PluginConfiguration config, ConfigurationEnvironment env, ConfigurationContext context) throws InitializeException {
      Class<?> pluginClass = config.getPluginClass();
      if (UserPersisterConfiguration.class.isAssignableFrom(pluginClass)) {
         return UserPersisterConfiguration.isUserPersisterExtended(config, env, context);
      }

      if (UserStoreConfiguration.class.isAssignableFrom(pluginClass)) {
         return UserStoreConfiguration.isInsertionAndDeletionUserStore(config, env, context);
      }

      LOG.debug("Unknown implementation of User Repository Configuration: " + pluginClass.getCanonicalName() + " - cannot determine capabilities");
      return false;
   }

   private static void checkThatARoleBelongsOnlyToOneUserPersister(PluginConfiguration config, ConfigurationEnvironment env, ConfigurationContext context) throws ConfigurationException {
      List<PluginConfiguration> adminRoleSpecificSettings = PluginListPropertyValueReader.effectivePluginListPropertyValue(
         config, "adminRoleSpecificSettings", env, context
      );
      Set<String> uniqueRoles = new HashSet<>();

      for (PluginConfiguration adminRoleSpecificSetting : adminRoleSpecificSettings) {
         List<String> roles = adminRoleSpecificSetting.getSimpleListPropertyConfigurationValue("requiredRoles", env, context);
         if (roles != null) {
            for (String role : roles) {
               if (!uniqueRoles.add(role)) {
                  throw new PropertyConstraintViolationException(
                     "adminRoleSpecificSettings",
                     config,
                     env,
                     context,
                     "Within the admin role specific settings at least one role is defined multiple times which is not allowed."
                  );
               }
            }
         }
      }
   }

   private static void checkThatConfiguredDefaultSortColumnExistsIfConfigured(
      PluginConfiguration config, ConfigurationEnvironment env, ConfigurationContext context
   ) throws ConfigurationException {
      String configuredDefaultSortColumn = StringPropertyValueReader.effectiveStringConfigValue(config, "defaultSortColumn", env, context);
      if (!StringUtils.isBlank(configuredDefaultSortColumn) && !Arrays.asList(DEFAULT_SORT_COLUMN_SUGGESTED_VALUES).contains(configuredDefaultSortColumn)) {
         for (PluginConfiguration configuredColumn : PluginListPropertyValueReader.effectivePluginListPropertyValue(config, "columnsInUserList", env, context)) {
            String columnPropertyName = StringPropertyValueReader.effectiveStringConfigValue(configuredColumn, "propertyName", env, context);
            boolean columnSortable = BooleanPropertyValueReader.effectiveBooleanPropertyValue(configuredColumn, "sortable", env, context);
            if (configuredDefaultSortColumn.equals(columnPropertyName) && columnSortable) {
               return;
            }
         }

         PluginConfiguration usernameSearchField = PluginPropertyValueReader.effectivePluginPropertyValue(config, "usernameSearchField", env, context);
         if (usernameSearchField != null) {
            String contextDataName = StringPropertyValueReader.effectiveStringConfigValue(usernameSearchField, "contextDataName", env, context);
            if (configuredDefaultSortColumn.equals(contextDataName)) {
               return;
            }
         }

         throw new PropertyConstraintViolationException(
            "defaultSortColumn",
            config,
            env,
            context,
            "The configured sort column must either be contained in \"Columns In User List\" (and flagged as sortable) or must match the \"Username Search Field\"."
         );
      }
   }

   private static void checkUserManagementIdUnique(PluginConfiguration config, ConfigurationEnvironment env, ConfigurationContext context) throws PropertyConfigurationException {
      List<PluginConfiguration> userManagementExtensionConfigs = PluginListPropertyValueReader.effectivePluginListPropertyValue(
         config, "userManagementExtensions", env, context
      );
      Set<String> usedIds = new HashSet<>();

      for (PluginConfiguration umeConfig : userManagementExtensionConfigs) {
         String id = StringPropertyValueReader.effectiveStringConfigValue(umeConfig, "id", env, context);
         if (!usedIds.add(id)) {
            throw new PropertyConstraintViolationException("id", umeConfig, env, context, "The ID must be unique across all User Management Extensions.");
         }
      }
   }

   private static void checkSearchItemsInListColumns(PluginConfiguration config, ConfigurationEnvironment env, ConfigurationContext context) throws PropertyConfigurationException {
      List<PluginConfiguration> columnsInUserList = PluginListPropertyValueReader.effectivePluginListPropertyValue(config, "columnsInUserList", env, context);
      Set<String> propertyNamesOfUserList = new HashSet<>();

      for (PluginConfiguration columnConfig : columnsInUserList) {
         propertyNamesOfUserList.add(StringPropertyValueReader.effectiveStringConfigValue(columnConfig, "propertyName", env, context));
      }

      for (PluginConfiguration userSearchProfileItem : PluginListPropertyValueReader.effectivePluginListPropertyValue(
         config, "userSearchProfileItems", env, context
      )) {
         String userSearchPropertyName = StringPropertyValueReader.effectiveStringConfigValue(userSearchProfileItem, "propertyName", env, context);
         if (!propertyNamesOfUserList.contains(userSearchPropertyName)) {
            throw new PropertyConstraintViolationException(
               "userSearchProfileItems",
               config,
               env,
               context,
               "Search Profile Item for property " + LogUtil.quote(userSearchPropertyName) + " not found in Columns In User List."
            );
         }
      }
   }

   private static void checkRememberMeConfigMandatory(PluginConfiguration config, ConfigurationEnvironment env, ConfigurationContext context) throws PropertyConfigurationException {
      PluginConfiguration flowRememberMeSettingsPlugin = PluginPropertyValueReader.effectivePluginPropertyValue(config, "rememberMeSettings", env, context);
      if (flowRememberMeSettingsPlugin == null) {
         boolean isFlowRememberMeManagementEnabled = BooleanPropertyValueReader.effectiveBooleanPropertyValue(
            config, "rememberMeManagementEnabled", env, context
         );
         if (isFlowRememberMeManagementEnabled) {
            throw new PropertyConstraintViolationException(
               "rememberMeSettings", config, env, context, "Remember-Me Settings is mandatory if rememberMeManagementEnabled is enabled."
            );
         }
      }
   }

   public Optional<UserGroupConfiguration> getGroupConfigurationForUser(PersistentUser user) {
      Objects.requireNonNull(user, "user required");
      return this.groupSettings.stream().filter(group -> group.useForUser(user)).findFirst();
   }

   public UserRepositoryConfiguration getUserRepositorySettings(Set<Role> adminRoles) {
      return this.roleSpecificConfigSelector.getUserRepositorySettings(adminRoles);
   }

   public List<UserProfileItemConfig> getColumnsInUserList() {
      return this.columnsInUserList;
   }

   public UserStore getConfiguredUserStore() {
      return this.userDataSource.getUserStore();
   }

   public <T extends UserSearchFilter<?>> List<T> getAdvancedSearchFilters() {
      return this.advancedSearchFilters.stream().map(f -> (UserSearchFilter)f).collect(Collectors.toList());
   }

   public Optional<JspRememberMeConfig> getJspLoginappRememberMeSettings() {
      return Optional.ofNullable(this.jspLoginappRememberMeSettings);
   }

   public boolean isRememberMeManagementEnabled() {
      return this.rememberMeManagementEnabled;
   }

   public Optional<RememberMeConfig> getFlowRememberMeSettings() {
      return Optional.ofNullable(this.rememberMeSettings);
   }

   public Optional<UsernameValidatorConfig> getUsernameValidator() {
      return Optional.ofNullable(this.usernameValidator);
   }

   public List<TokenController> getAuthenticationTokens() {
      return this.authenticationTokens;
   }

   public boolean isEditable() {
      return this.editable;
   }

   public List<String> getAvailableUserRoles(Set<Role> adminRoles) {
      return this.roleSpecificConfigSelector.getAvailableUserRoles(adminRoles);
   }

   public List<String> getDefaultAvailableUserRoles() {
      return this.availableUserRoles;
   }

   public boolean isShowUserValidSection() {
      return this.showUserValidSection;
   }

   public UserLockedVisibility getUserLockedSection() {
      return this.userLockedSection;
   }

   public Optional<String> getLanguageContextKey() {
      return Optional.ofNullable(this.languageContextKey);
   }

   public int getMaxUsersToList() {
      return this.maxUsersToList;
   }

   public Optional<StringContextDataItemNameConfig> getUsernameSearchField() {
      return Optional.ofNullable(this.usernameSearchField);
   }

   public Optional<String> getDefaultSortColumn() {
      return Optional.ofNullable(this.defaultSortColumn);
   }

   public boolean isSortAscendingByDefault() {
      return this.sortAscendingByDefault;
   }

   public List<UserProfileItemSearchConfig> getUserSearchProfileItems() {
      return this.userSearchProfileItems;
   }

   public List<UserProfileItemConfig> getRowsOnUserDetailPage() {
      return this.rowsOnUserDetailPage;
   }

   public UserRepositoryConfiguration getUserDataSource() {
      return this.userDataSource;
   }

   public Optional<AccountLinkManagementConfig> getAccountLinkManagementConfig() {
      return Optional.ofNullable(this.accountLinkManagementConfig);
   }

   public LockingSettings getLockingSettings() {
      return this.lockingSettings;
   }

   public boolean isShowMigrationSection() {
      return this.showMigrationSection;
   }

   public boolean isEnableMultipleNextAuthMethods() {
      return this.enableMultipleNextAuthMethods;
   }

   public List<UserManagementExtensionConfig> getUserManagementExtensions() {
      return this.userManagementExtensions;
   }

   public List<String> getAdditionalAuthMethodProperties() {
      return this.additionalAuthMethodProperties;
   }

   public boolean isAllowBulkChanges() {
      return this.allowBulkChanges;
   }

   public Optional<UserIdentityGenerator> getUserIdentityGenerator() {
      return Optional.ofNullable(this.userIdentityGenerator);
   }

   public Optional<UserProfileValueProviderConfig> getUsernamePrefill() {
      return Optional.ofNullable(this.usernamePrefill);
   }

   public boolean isCaseSensitiveSearchAsDefault() {
      return this.caseSensitiveSearchAsDefault;
   }

   public boolean isOnlySearchWordsAsDefault() {
      return this.onlySearchWordsAsDefault;
   }

   public boolean isSearchInUsernameAsDefault() {
      return this.searchInUsernameAsDefault;
   }

   public boolean isAutomaticallyPerformLastSearch() {
      return this.automaticallyPerformLastSearch;
   }

   public boolean isAdminCannotDeleteUserWithSameName() {
      return this.adminCannotDeleteUserWithSameName;
   }

   public Optional<UsersExportConfig> getExport() {
      return Optional.ofNullable(this.export);
   }

   public List<UsersOAuth2AuthorizationServerConfig> getOauth2AuthorizationServers() {
      return this.oauth2AuthorizationServers;
   }

   @SuppressFBWarnings(justification = "generated code")
   public static UsersConfiguration.UsersConfigurationBuilder builder() {
      return new UsersConfiguration.UsersConfigurationBuilder();
   }

   @SuppressFBWarnings(justification = "generated code")
   public UsersConfiguration() {
   }

   @SuppressFBWarnings(justification = "generated code")
   public UsersConfiguration(
      List<TokenController> authenticationTokens,
      boolean editable,
      List<String> availableUserRoles,
      boolean showUserValidSection,
      UserLockedVisibility userLockedSection,
      String languageContextKey,
      int maxUsersToList,
      StringContextDataItemNameConfig usernameSearchField,
      String defaultSortColumn,
      boolean sortAscendingByDefault,
      List<UserProfileItemConfig> columnsInUserList,
      List<UserProfileItemSearchConfig> userSearchProfileItems,
      List<UserProfileItemConfig> rowsOnUserDetailPage,
      UserRepositoryConfiguration userDataSource,
      List<AdminRoleSpecificSetting> adminRoleSpecificSettings,
      RolebasedConfigSelectionStrategy roleSpecificSettingsSelection,
      AccountLinkManagementConfig accountLinkManagementConfig,
      LockingSettings lockingSettings,
      boolean showMigrationSection,
      boolean enableMultipleNextAuthMethods,
      List<UserManagementExtensionConfig> userManagementExtensions,
      List<String> additionalAuthMethodProperties,
      boolean allowBulkChanges,
      UserIdentityGenerator userIdentityGenerator,
      UserProfileValueProviderConfig usernamePrefill,
      UsernameValidatorConfig usernameValidator,
      List<UserGroupConfiguration> groupSettings,
      boolean caseSensitiveSearchAsDefault,
      boolean onlySearchWordsAsDefault,
      boolean searchInUsernameAsDefault,
      List<UserSearchFilter> advancedSearchFilters,
      boolean automaticallyPerformLastSearch,
      UsersExportConfig export,
      boolean rememberMeManagementEnabled,
      RememberMeConfig rememberMeSettings,
      JspRememberMeConfig jspLoginappRememberMeSettings,
      List<UsersOAuth2AuthorizationServerConfig> oauth2AuthorizationServers,
      boolean adminCannotDeleteUserWithSameName,
      RoleSpecificConfigSelector roleSpecificConfigSelector
   ) {
      this.authenticationTokens = authenticationTokens;
      this.editable = editable;
      this.availableUserRoles = availableUserRoles;
      this.showUserValidSection = showUserValidSection;
      this.userLockedSection = userLockedSection;
      this.languageContextKey = languageContextKey;
      this.maxUsersToList = maxUsersToList;
      this.usernameSearchField = usernameSearchField;
      this.defaultSortColumn = defaultSortColumn;
      this.sortAscendingByDefault = sortAscendingByDefault;
      this.columnsInUserList = columnsInUserList;
      this.userSearchProfileItems = userSearchProfileItems;
      this.rowsOnUserDetailPage = rowsOnUserDetailPage;
      this.userDataSource = userDataSource;
      this.adminRoleSpecificSettings = adminRoleSpecificSettings;
      this.roleSpecificSettingsSelection = roleSpecificSettingsSelection;
      this.accountLinkManagementConfig = accountLinkManagementConfig;
      this.lockingSettings = lockingSettings;
      this.showMigrationSection = showMigrationSection;
      this.enableMultipleNextAuthMethods = enableMultipleNextAuthMethods;
      this.userManagementExtensions = userManagementExtensions;
      this.additionalAuthMethodProperties = additionalAuthMethodProperties;
      this.allowBulkChanges = allowBulkChanges;
      this.userIdentityGenerator = userIdentityGenerator;
      this.usernamePrefill = usernamePrefill;
      this.usernameValidator = usernameValidator;
      this.groupSettings = groupSettings;
      this.caseSensitiveSearchAsDefault = caseSensitiveSearchAsDefault;
      this.onlySearchWordsAsDefault = onlySearchWordsAsDefault;
      this.searchInUsernameAsDefault = searchInUsernameAsDefault;
      this.advancedSearchFilters = advancedSearchFilters;
      this.automaticallyPerformLastSearch = automaticallyPerformLastSearch;
      this.export = export;
      this.rememberMeManagementEnabled = rememberMeManagementEnabled;
      this.rememberMeSettings = rememberMeSettings;
      this.jspLoginappRememberMeSettings = jspLoginappRememberMeSettings;
      this.oauth2AuthorizationServers = oauth2AuthorizationServers;
      this.adminCannotDeleteUserWithSameName = adminCannotDeleteUserWithSameName;
      this.roleSpecificConfigSelector = roleSpecificConfigSelector;
   }

   @SuppressFBWarnings(justification = "generated code")
   public static class UsersConfigurationBuilder {
      @SuppressFBWarnings(justification = "generated code")
      private ArrayList<TokenController> authenticationTokens;
      @SuppressFBWarnings(justification = "generated code")
      private boolean editable;
      @SuppressFBWarnings(justification = "generated code")
      private List<String> availableUserRoles;
      @SuppressFBWarnings(justification = "generated code")
      private boolean showUserValidSection;
      @SuppressFBWarnings(justification = "generated code")
      private UserLockedVisibility userLockedSection;
      @SuppressFBWarnings(justification = "generated code")
      private String languageContextKey;
      @SuppressFBWarnings(justification = "generated code")
      private int maxUsersToList;
      @SuppressFBWarnings(justification = "generated code")
      private StringContextDataItemNameConfig usernameSearchField;
      @SuppressFBWarnings(justification = "generated code")
      private String defaultSortColumn;
      @SuppressFBWarnings(justification = "generated code")
      private boolean sortAscendingByDefault;
      @SuppressFBWarnings(justification = "generated code")
      private List<UserProfileItemConfig> columnsInUserList;
      @SuppressFBWarnings(justification = "generated code")
      private List<UserProfileItemSearchConfig> userSearchProfileItems;
      @SuppressFBWarnings(justification = "generated code")
      private List<UserProfileItemConfig> rowsOnUserDetailPage;
      @SuppressFBWarnings(justification = "generated code")
      private UserRepositoryConfiguration userDataSource;
      @SuppressFBWarnings(justification = "generated code")
      private List<AdminRoleSpecificSetting> adminRoleSpecificSettings;
      @SuppressFBWarnings(justification = "generated code")
      private RolebasedConfigSelectionStrategy roleSpecificSettingsSelection;
      @SuppressFBWarnings(justification = "generated code")
      private AccountLinkManagementConfig accountLinkManagementConfig;
      @SuppressFBWarnings(justification = "generated code")
      private LockingSettings lockingSettings;
      @SuppressFBWarnings(justification = "generated code")
      private boolean showMigrationSection;
      @SuppressFBWarnings(justification = "generated code")
      private boolean enableMultipleNextAuthMethods;
      @SuppressFBWarnings(justification = "generated code")
      private List<UserManagementExtensionConfig> userManagementExtensions;
      @SuppressFBWarnings(justification = "generated code")
      private List<String> additionalAuthMethodProperties;
      @SuppressFBWarnings(justification = "generated code")
      private boolean allowBulkChanges;
      @SuppressFBWarnings(justification = "generated code")
      private UserIdentityGenerator userIdentityGenerator;
      @SuppressFBWarnings(justification = "generated code")
      private UserProfileValueProviderConfig usernamePrefill;
      @SuppressFBWarnings(justification = "generated code")
      private UsernameValidatorConfig usernameValidator;
      @SuppressFBWarnings(justification = "generated code")
      private List<UserGroupConfiguration> groupSettings;
      @SuppressFBWarnings(justification = "generated code")
      private boolean caseSensitiveSearchAsDefault;
      @SuppressFBWarnings(justification = "generated code")
      private boolean onlySearchWordsAsDefault;
      @SuppressFBWarnings(justification = "generated code")
      private boolean searchInUsernameAsDefault;
      @SuppressFBWarnings(justification = "generated code")
      private List<UserSearchFilter> advancedSearchFilters;
      @SuppressFBWarnings(justification = "generated code")
      private boolean automaticallyPerformLastSearch;
      @SuppressFBWarnings(justification = "generated code")
      private UsersExportConfig export;
      @SuppressFBWarnings(justification = "generated code")
      private boolean rememberMeManagementEnabled;
      @SuppressFBWarnings(justification = "generated code")
      private RememberMeConfig rememberMeSettings;
      @SuppressFBWarnings(justification = "generated code")
      private JspRememberMeConfig jspLoginappRememberMeSettings;
      @SuppressFBWarnings(justification = "generated code")
      private List<UsersOAuth2AuthorizationServerConfig> oauth2AuthorizationServers;
      @SuppressFBWarnings(justification = "generated code")
      private boolean adminCannotDeleteUserWithSameName;

      @SuppressFBWarnings(justification = "generated code")
      UsersConfigurationBuilder() {
      }

      @SuppressFBWarnings(justification = "generated code")
      public UsersConfiguration.UsersConfigurationBuilder authenticationToken(TokenController authenticationToken) {
         if (this.authenticationTokens == null) {
            this.authenticationTokens = new ArrayList<>();
         }

         this.authenticationTokens.add(authenticationToken);
         return this;
      }

      @SuppressFBWarnings(justification = "generated code")
      public UsersConfiguration.UsersConfigurationBuilder authenticationTokens(Collection<? extends TokenController> authenticationTokens) {
         if (authenticationTokens == null) {
            throw new NullPointerException("authenticationTokens cannot be null");
         }

         if (this.authenticationTokens == null) {
            this.authenticationTokens = new ArrayList<>();
         }

         this.authenticationTokens.addAll(authenticationTokens);
         return this;
      }

      @SuppressFBWarnings(justification = "generated code")
      public UsersConfiguration.UsersConfigurationBuilder clearAuthenticationTokens() {
         if (this.authenticationTokens != null) {
            this.authenticationTokens.clear();
         }

         return this;
      }

      @SuppressFBWarnings(justification = "generated code")
      public UsersConfiguration.UsersConfigurationBuilder editable(boolean editable) {
         this.editable = editable;
         return this;
      }

      @SuppressFBWarnings(justification = "generated code")
      public UsersConfiguration.UsersConfigurationBuilder availableUserRoles(List<String> availableUserRoles) {
         this.availableUserRoles = availableUserRoles;
         return this;
      }

      @SuppressFBWarnings(justification = "generated code")
      public UsersConfiguration.UsersConfigurationBuilder showUserValidSection(boolean showUserValidSection) {
         this.showUserValidSection = showUserValidSection;
         return this;
      }

      @SuppressFBWarnings(justification = "generated code")
      public UsersConfiguration.UsersConfigurationBuilder userLockedSection(UserLockedVisibility userLockedSection) {
         this.userLockedSection = userLockedSection;
         return this;
      }

      @SuppressFBWarnings(justification = "generated code")
      public UsersConfiguration.UsersConfigurationBuilder languageContextKey(String languageContextKey) {
         this.languageContextKey = languageContextKey;
         return this;
      }

      @SuppressFBWarnings(justification = "generated code")
      public UsersConfiguration.UsersConfigurationBuilder maxUsersToList(int maxUsersToList) {
         this.maxUsersToList = maxUsersToList;
         return this;
      }

      @SuppressFBWarnings(justification = "generated code")
      public UsersConfiguration.UsersConfigurationBuilder usernameSearchField(StringContextDataItemNameConfig usernameSearchField) {
         this.usernameSearchField = usernameSearchField;
         return this;
      }

      @SuppressFBWarnings(justification = "generated code")
      public UsersConfiguration.UsersConfigurationBuilder defaultSortColumn(String defaultSortColumn) {
         this.defaultSortColumn = defaultSortColumn;
         return this;
      }

      @SuppressFBWarnings(justification = "generated code")
      public UsersConfiguration.UsersConfigurationBuilder sortAscendingByDefault(boolean sortAscendingByDefault) {
         this.sortAscendingByDefault = sortAscendingByDefault;
         return this;
      }

      @SuppressFBWarnings(justification = "generated code")
      public UsersConfiguration.UsersConfigurationBuilder columnsInUserList(List<UserProfileItemConfig> columnsInUserList) {
         this.columnsInUserList = columnsInUserList;
         return this;
      }

      @SuppressFBWarnings(justification = "generated code")
      public UsersConfiguration.UsersConfigurationBuilder userSearchProfileItems(List<UserProfileItemSearchConfig> userSearchProfileItems) {
         this.userSearchProfileItems = userSearchProfileItems;
         return this;
      }

      @SuppressFBWarnings(justification = "generated code")
      public UsersConfiguration.UsersConfigurationBuilder rowsOnUserDetailPage(List<UserProfileItemConfig> rowsOnUserDetailPage) {
         this.rowsOnUserDetailPage = rowsOnUserDetailPage;
         return this;
      }

      @SuppressFBWarnings(justification = "generated code")
      public UsersConfiguration.UsersConfigurationBuilder userDataSource(UserRepositoryConfiguration userDataSource) {
         this.userDataSource = userDataSource;
         return this;
      }

      @SuppressFBWarnings(justification = "generated code")
      public UsersConfiguration.UsersConfigurationBuilder adminRoleSpecificSettings(List<AdminRoleSpecificSetting> adminRoleSpecificSettings) {
         this.adminRoleSpecificSettings = adminRoleSpecificSettings;
         return this;
      }

      @SuppressFBWarnings(justification = "generated code")
      public UsersConfiguration.UsersConfigurationBuilder roleSpecificSettingsSelection(RolebasedConfigSelectionStrategy roleSpecificSettingsSelection) {
         this.roleSpecificSettingsSelection = roleSpecificSettingsSelection;
         return this;
      }

      @SuppressFBWarnings(justification = "generated code")
      public UsersConfiguration.UsersConfigurationBuilder accountLinkManagementConfig(AccountLinkManagementConfig accountLinkManagementConfig) {
         this.accountLinkManagementConfig = accountLinkManagementConfig;
         return this;
      }

      @SuppressFBWarnings(justification = "generated code")
      public UsersConfiguration.UsersConfigurationBuilder lockingSettings(LockingSettings lockingSettings) {
         this.lockingSettings = lockingSettings;
         return this;
      }

      @SuppressFBWarnings(justification = "generated code")
      public UsersConfiguration.UsersConfigurationBuilder showMigrationSection(boolean showMigrationSection) {
         this.showMigrationSection = showMigrationSection;
         return this;
      }

      @SuppressFBWarnings(justification = "generated code")
      public UsersConfiguration.UsersConfigurationBuilder enableMultipleNextAuthMethods(boolean enableMultipleNextAuthMethods) {
         this.enableMultipleNextAuthMethods = enableMultipleNextAuthMethods;
         return this;
      }

      @SuppressFBWarnings(justification = "generated code")
      public UsersConfiguration.UsersConfigurationBuilder userManagementExtensions(List<UserManagementExtensionConfig> userManagementExtensions) {
         this.userManagementExtensions = userManagementExtensions;
         return this;
      }

      @SuppressFBWarnings(justification = "generated code")
      public UsersConfiguration.UsersConfigurationBuilder additionalAuthMethodProperties(List<String> additionalAuthMethodProperties) {
         this.additionalAuthMethodProperties = additionalAuthMethodProperties;
         return this;
      }

      @SuppressFBWarnings(justification = "generated code")
      public UsersConfiguration.UsersConfigurationBuilder allowBulkChanges(boolean allowBulkChanges) {
         this.allowBulkChanges = allowBulkChanges;
         return this;
      }

      @SuppressFBWarnings(justification = "generated code")
      public UsersConfiguration.UsersConfigurationBuilder userIdentityGenerator(UserIdentityGenerator userIdentityGenerator) {
         this.userIdentityGenerator = userIdentityGenerator;
         return this;
      }

      @SuppressFBWarnings(justification = "generated code")
      public UsersConfiguration.UsersConfigurationBuilder usernamePrefill(UserProfileValueProviderConfig usernamePrefill) {
         this.usernamePrefill = usernamePrefill;
         return this;
      }

      @SuppressFBWarnings(justification = "generated code")
      public UsersConfiguration.UsersConfigurationBuilder usernameValidator(UsernameValidatorConfig usernameValidator) {
         this.usernameValidator = usernameValidator;
         return this;
      }

      @SuppressFBWarnings(justification = "generated code")
      public UsersConfiguration.UsersConfigurationBuilder groupSettings(List<UserGroupConfiguration> groupSettings) {
         this.groupSettings = groupSettings;
         return this;
      }

      @SuppressFBWarnings(justification = "generated code")
      public UsersConfiguration.UsersConfigurationBuilder caseSensitiveSearchAsDefault(boolean caseSensitiveSearchAsDefault) {
         this.caseSensitiveSearchAsDefault = caseSensitiveSearchAsDefault;
         return this;
      }

      @SuppressFBWarnings(justification = "generated code")
      public UsersConfiguration.UsersConfigurationBuilder onlySearchWordsAsDefault(boolean onlySearchWordsAsDefault) {
         this.onlySearchWordsAsDefault = onlySearchWordsAsDefault;
         return this;
      }

      @SuppressFBWarnings(justification = "generated code")
      public UsersConfiguration.UsersConfigurationBuilder searchInUsernameAsDefault(boolean searchInUsernameAsDefault) {
         this.searchInUsernameAsDefault = searchInUsernameAsDefault;
         return this;
      }

      @SuppressFBWarnings(justification = "generated code")
      public UsersConfiguration.UsersConfigurationBuilder advancedSearchFilters(List<UserSearchFilter> advancedSearchFilters) {
         this.advancedSearchFilters = advancedSearchFilters;
         return this;
      }

      @SuppressFBWarnings(justification = "generated code")
      public UsersConfiguration.UsersConfigurationBuilder automaticallyPerformLastSearch(boolean automaticallyPerformLastSearch) {
         this.automaticallyPerformLastSearch = automaticallyPerformLastSearch;
         return this;
      }

      @SuppressFBWarnings(justification = "generated code")
      public UsersConfiguration.UsersConfigurationBuilder export(UsersExportConfig export) {
         this.export = export;
         return this;
      }

      @SuppressFBWarnings(justification = "generated code")
      public UsersConfiguration.UsersConfigurationBuilder rememberMeManagementEnabled(boolean rememberMeManagementEnabled) {
         this.rememberMeManagementEnabled = rememberMeManagementEnabled;
         return this;
      }

      @SuppressFBWarnings(justification = "generated code")
      public UsersConfiguration.UsersConfigurationBuilder rememberMeSettings(RememberMeConfig rememberMeSettings) {
         this.rememberMeSettings = rememberMeSettings;
         return this;
      }

      @SuppressFBWarnings(justification = "generated code")
      public UsersConfiguration.UsersConfigurationBuilder jspLoginappRememberMeSettings(JspRememberMeConfig jspLoginappRememberMeSettings) {
         this.jspLoginappRememberMeSettings = jspLoginappRememberMeSettings;
         return this;
      }

      @SuppressFBWarnings(justification = "generated code")
      public UsersConfiguration.UsersConfigurationBuilder oauth2AuthorizationServers(List<UsersOAuth2AuthorizationServerConfig> oauth2AuthorizationServers) {
         this.oauth2AuthorizationServers = oauth2AuthorizationServers;
         return this;
      }

      @SuppressFBWarnings(justification = "generated code")
      public UsersConfiguration.UsersConfigurationBuilder adminCannotDeleteUserWithSameName(boolean adminCannotDeleteUserWithSameName) {
         this.adminCannotDeleteUserWithSameName = adminCannotDeleteUserWithSameName;
         return this;
      }

      @SuppressFBWarnings(justification = "generated code")
      public UsersConfiguration build() {
         return new UsersConfiguration(
            switch (this.authenticationTokens == null ? 0 : this.authenticationTokens.size()) {
               case 0 -> Collections.emptyList();
               case 1 -> Collections.singletonList(this.authenticationTokens.get(0));
               default -> Collections.unmodifiableList(new ArrayList<>(this.authenticationTokens));
            },
            this.editable,
            this.availableUserRoles,
            this.showUserValidSection,
            this.userLockedSection,
            this.languageContextKey,
            this.maxUsersToList,
            this.usernameSearchField,
            this.defaultSortColumn,
            this.sortAscendingByDefault,
            this.columnsInUserList,
            this.userSearchProfileItems,
            this.rowsOnUserDetailPage,
            this.userDataSource,
            this.adminRoleSpecificSettings,
            this.roleSpecificSettingsSelection,
            this.accountLinkManagementConfig,
            this.lockingSettings,
            this.showMigrationSection,
            this.enableMultipleNextAuthMethods,
            this.userManagementExtensions,
            this.additionalAuthMethodProperties,
            this.allowBulkChanges,
            this.userIdentityGenerator,
            this.usernamePrefill,
            this.usernameValidator,
            this.groupSettings,
            this.caseSensitiveSearchAsDefault,
            this.onlySearchWordsAsDefault,
            this.searchInUsernameAsDefault,
            this.advancedSearchFilters,
            this.automaticallyPerformLastSearch,
            this.export,
            this.rememberMeManagementEnabled,
            this.rememberMeSettings,
            this.jspLoginappRememberMeSettings,
            this.oauth2AuthorizationServers,
            this.adminCannotDeleteUserWithSameName
         );
      }

      @SuppressFBWarnings(justification = "generated code")
      @Override
      public String toString() {
         return "UsersConfiguration.UsersConfigurationBuilder(authenticationTokens="
            + this.authenticationTokens
            + ", editable="
            + this.editable
            + ", availableUserRoles="
            + this.availableUserRoles
            + ", showUserValidSection="
            + this.showUserValidSection
            + ", userLockedSection="
            + this.userLockedSection
            + ", languageContextKey="
            + this.languageContextKey
            + ", maxUsersToList="
            + this.maxUsersToList
            + ", usernameSearchField="
            + this.usernameSearchField
            + ", defaultSortColumn="
            + this.defaultSortColumn
            + ", sortAscendingByDefault="
            + this.sortAscendingByDefault
            + ", columnsInUserList="
            + this.columnsInUserList
            + ", userSearchProfileItems="
            + this.userSearchProfileItems
            + ", rowsOnUserDetailPage="
            + this.rowsOnUserDetailPage
            + ", userDataSource="
            + this.userDataSource
            + ", adminRoleSpecificSettings="
            + this.adminRoleSpecificSettings
            + ", roleSpecificSettingsSelection="
            + this.roleSpecificSettingsSelection
            + ", accountLinkManagementConfig="
            + this.accountLinkManagementConfig
            + ", lockingSettings="
            + this.lockingSettings
            + ", showMigrationSection="
            + this.showMigrationSection
            + ", enableMultipleNextAuthMethods="
            + this.enableMultipleNextAuthMethods
            + ", userManagementExtensions="
            + this.userManagementExtensions
            + ", additionalAuthMethodProperties="
            + this.additionalAuthMethodProperties
            + ", allowBulkChanges="
            + this.allowBulkChanges
            + ", userIdentityGenerator="
            + this.userIdentityGenerator
            + ", usernamePrefill="
            + this.usernamePrefill
            + ", usernameValidator="
            + this.usernameValidator
            + ", groupSettings="
            + this.groupSettings
            + ", caseSensitiveSearchAsDefault="
            + this.caseSensitiveSearchAsDefault
            + ", onlySearchWordsAsDefault="
            + this.onlySearchWordsAsDefault
            + ", searchInUsernameAsDefault="
            + this.searchInUsernameAsDefault
            + ", advancedSearchFilters="
            + this.advancedSearchFilters
            + ", automaticallyPerformLastSearch="
            + this.automaticallyPerformLastSearch
            + ", export="
            + this.export
            + ", rememberMeManagementEnabled="
            + this.rememberMeManagementEnabled
            + ", rememberMeSettings="
            + this.rememberMeSettings
            + ", jspLoginappRememberMeSettings="
            + this.jspLoginappRememberMeSettings
            + ", oauth2AuthorizationServers="
            + this.oauth2AuthorizationServers
            + ", adminCannotDeleteUserWithSameName="
            + this.adminCannotDeleteUserWithSameName
            + ")";
      }
   }
}
