diff --git a/src/main/java/kst4contest/view/CommandLineOptions.java b/src/main/java/kst4contest/view/CommandLineOptions.java
new file mode 100644
index 00000000..b0c571b2
--- /dev/null
+++ b/src/main/java/kst4contest/view/CommandLineOptions.java
@@ -0,0 +1,102 @@
+package kst4contest.view;
+
+import java.util.List;
+
+/**
+ * Command line options of the application.
+ *
+ *
The parsed value is additionally kept in a process wide holder. JavaFX only knows
+ * the parameters of the {@code Application} instance it launched itself, so an instance
+ * created during a profile switch would see no parameters at all. Parsing once at
+ * startup and remembering the result avoids that entirely.
+ */
+public class CommandLineOptions {
+
+ /**
+ * Command line switch selecting the operator profile to start with.
+ */
+ public static final String PROFILE_ARGUMENT = "--profile";
+
+ /**
+ * System property used as an alternative to the command line switch.
+ */
+ public static final String PROFILE_SYSTEM_PROPERTY = "kst4contest.profile";
+
+ private static volatile CommandLineOptions rememberedOptions = new CommandLineOptions(null);
+
+ private final String requestedProfileName;
+
+ public CommandLineOptions(final String requestedProfileName) {
+ this.requestedProfileName = requestedProfileName;
+ }
+
+ /**
+ * Parses the raw application arguments.
+ *
+ * Unknown arguments are ignored on purpose. A typo in a command line must never
+ * keep an operator out of the application shortly before a contest.
+ *
+ * @param rawArguments raw arguments, may be null
+ * @return the parsed options
+ */
+ public static CommandLineOptions parse(final List rawArguments) {
+
+ String requestedProfileName = null;
+
+ if (rawArguments != null) {
+ for (int argumentIndex = 0; argumentIndex < rawArguments.size(); argumentIndex++) {
+ String currentArgument = rawArguments.get(argumentIndex);
+
+ if (currentArgument == null) {
+ continue;
+ }
+
+ if (currentArgument.startsWith(PROFILE_ARGUMENT + "=")) {
+ requestedProfileName = currentArgument.substring(PROFILE_ARGUMENT.length() + 1);
+ } else if (PROFILE_ARGUMENT.equals(currentArgument)
+ && argumentIndex + 1 < rawArguments.size()) {
+ requestedProfileName = rawArguments.get(argumentIndex + 1);
+ argumentIndex++;
+ }
+ }
+ }
+
+ if (requestedProfileName == null || requestedProfileName.isBlank()) {
+ requestedProfileName = System.getProperty(PROFILE_SYSTEM_PROPERTY);
+ }
+
+ if (requestedProfileName != null && requestedProfileName.isBlank()) {
+ requestedProfileName = null;
+ }
+
+ return new CommandLineOptions(
+ requestedProfileName == null ? null : requestedProfileName.trim());
+ }
+
+ /**
+ * Stores the parsed options for the lifetime of the process.
+ *
+ * @param options options to remember
+ */
+ public static void remember(final CommandLineOptions options) {
+ rememberedOptions = options == null ? new CommandLineOptions(null) : options;
+ }
+
+ /**
+ * Returns the options parsed at application startup.
+ *
+ * @return the remembered options, never null
+ */
+ public static CommandLineOptions remembered() {
+ return rememberedOptions;
+ }
+
+ /**
+ * Returns the operator profile requested on the command line.
+ *
+ * @return the requested profile name, or null when none was given
+ */
+ public String getRequestedProfileName() {
+ return requestedProfileName;
+ }
+}
diff --git a/src/main/java/kst4contest/view/Kst4ContestApplication.java b/src/main/java/kst4contest/view/Kst4ContestApplication.java
index 91debdc6..fcdcaf00 100644
--- a/src/main/java/kst4contest/view/Kst4ContestApplication.java
+++ b/src/main/java/kst4contest/view/Kst4ContestApplication.java
@@ -70,6 +70,9 @@ import javafx.stage.Screen;
import kst4contest.logic.BandOpportunityResolver;
import kst4contest.utils.ApplicationFileUtils;
import kst4contest.view.map.StationMapBridge;
+import kst4contest.controller.ActiveOperatorProfile;
+import kst4contest.controller.OperatorProfileStore;
+import kst4contest.model.OperatorProfileSelection;
import kst4contest.view.map.StationMapView;
import kst4contest.view.map.OfflineDemImportService;
import kst4contest.controller.WorkedGrossFieldCache;
@@ -6227,6 +6230,66 @@ public class Kst4ContestApplication extends Application implements StatusUpdateL
return txMessageButtons;
}
+ /**
+ * Resolves the operator profile this runtime works with.
+ *
+ * Only executed on the very first launch. A profile switch sets the profile before
+ * building the new runtime, so the resolution is skipped there.
+ *
+ * An installation with no or exactly one profile is resolved without asking
+ * anything, which keeps the single operator startup exactly as it was.
+ *
+ * @return true if the application may continue starting up
+ */
+ private boolean resolveOperatorProfileIfRequired() {
+
+ if (ActiveOperatorProfile.isInitialized()) {
+ return true;
+ }
+
+ OperatorProfileBootstrap bootstrap = new OperatorProfileBootstrap();
+ OperatorProfileSelection resolvedProfile = bootstrap.resolveAtStartup(
+ new OperatorProfileStore(),
+ CommandLineOptions.remembered(),
+ OperatorProfilePickerDialog::showAndSelect);
+
+ if (bootstrap.getStartupWarning() != null) {
+ Alert startupWarning = new Alert(AlertType.WARNING);
+ startupWarning.setTitle("Operator profile");
+ startupWarning.setHeaderText("The requested operator profile was not found.");
+ startupWarning.setContentText(bootstrap.getStartupWarning());
+ startupWarning.showAndWait();
+ }
+
+ if (resolvedProfile == null) {
+ Platform.exit();
+ System.exit(0);
+ return false;
+ }
+
+ ActiveOperatorProfile.set(resolvedProfile);
+ return true;
+ }
+
+ /**
+ * Returns the window title suffix naming the active operator profile.
+ *
+ * Empty for the historic single profile installation, so nothing changes visually
+ * for operators who never create a second profile.
+ *
+ * @return the suffix to append to a window title, never null
+ */
+ private String buildOperatorProfileTitleSuffix() {
+
+ OperatorProfileSelection activeProfile = ActiveOperatorProfile.get();
+
+ if (activeProfile == null || activeProfile.getProfile().isRootProfile()) {
+ return "";
+ }
+
+ return " - " + activeProfile.getProfile().getDisplayName();
+ }
+
@Override
public void stop() {
System.out.println("[Main.java, Info:] Stage is closing, killing all resources");
@@ -6644,9 +6707,22 @@ public class Kst4ContestApplication extends Application implements StatusUpdateL
};
}
+ @Override
+ public void init() {
+
+ Parameters applicationParameters = getParameters();
+
+ CommandLineOptions.remember(CommandLineOptions.parse(
+ applicationParameters == null ? null : applicationParameters.getRaw()));
+ }
+
@Override
public void start(Stage primaryStage) throws InterruptedException, IOException, URISyntaxException {
+ if (!resolveOperatorProfileIfRequired()) {
+ return;
+ }
+
GuiUtils.applyApplicationIcon(primaryStage);
VBox pnl_inputAndSendButtons = new VBox(); //gets the sendtext field, send button and the timeline
@@ -6681,8 +6757,15 @@ public class Kst4ContestApplication extends Application implements StatusUpdateL
ApplicationFileUtils.copyResourceIfRequired(ApplicationConstants.APPLICATION_NAME, STYLE_DEFAULTCSSDAY_RESOURCE, STYLE_DEFAULTCSSDAY_FILE);
ApplicationFileUtils.copyResourceIfRequired(ApplicationConstants.APPLICATION_NAME, STYLE_DEFAULTCSSEVENING_RESOURCE, STYLE_DEFAULTCSSEVENING_FILE);
ChatMember ownChatMemberObject = new ChatMember();
+ OperatorProfileSelection activeOperatorProfile = ActiveOperatorProfile.get();
- chatcontroller = new ChatController(ownChatMemberObject, this); // instantiate the Chatcontroller with the user object
+ // instantiate the Chatcontroller with the user object and the files of the active profile
+ chatcontroller = new ChatController(
+ ownChatMemberObject,
+ this,
+ activeOperatorProfile.getPreferencesRelativeFileName(),
+ activeOperatorProfile.getWorkedDatabaseRelativeFileName(),
+ activeOperatorProfile.isSeedWorkedDatabaseFromResource());
layoutAutosave = new LayoutAutosave(chatcontroller.getChatPreferences());
messageVariableResolver = new MessageVariableResolver(chatcontroller.getChatPreferences());
chatcontroller.setStatusListener(this); //callback interface for updating Thread events in visual
@@ -7200,7 +7283,7 @@ public class Kst4ContestApplication extends Application implements StatusUpdateL
txt_ownqrgSecondCategory.setFocusTraversable(false);
txt_ownqrgSecondCategory.setTooltip(new Tooltip("Enter frequency for second chat-category here by hand! "));
- primaryStage.setTitle(chatcontroller.getChatPreferences().getChatState());
+ primaryStage.setTitle(chatcontroller.getChatPreferences().getChatState() + buildOperatorProfileTitleSuffix());
timer_buildWindowTitle = new Timer();
timer_buildWindowTitle.scheduleAtFixedRate(new TimerTask() {
@@ -7255,7 +7338,7 @@ public class Kst4ContestApplication extends Application implements StatusUpdateL
chatcontroller.getChatPreferences().setChatState(chatState);
}
- primaryStage.setTitle(chatcontroller.getChatPreferences().getChatState());
+ primaryStage.setTitle(chatcontroller.getChatPreferences().getChatState() + buildOperatorProfileTitleSuffix());
// System.out.println(chatcontroller.getChatPreferences().getChatState());
});
diff --git a/src/main/java/kst4contest/view/OperatorProfileBootstrap.java b/src/main/java/kst4contest/view/OperatorProfileBootstrap.java
new file mode 100644
index 00000000..22e022bb
--- /dev/null
+++ b/src/main/java/kst4contest/view/OperatorProfileBootstrap.java
@@ -0,0 +1,107 @@
+package kst4contest.view;
+
+import kst4contest.controller.OperatorProfilePaths;
+import kst4contest.controller.OperatorProfileStore;
+import kst4contest.model.OperatorProfile;
+import kst4contest.model.OperatorProfileSelection;
+
+import java.util.List;
+import java.util.Optional;
+
+/**
+ * Decides which operator profile the application starts with.
+ *
+ * The class contains no user interface code so the decision can be tested headless.
+ * Asking the operator is delegated to an {@link OperatorProfileChoiceRequester}, and a
+ * problem worth telling the operator about is reported through
+ * {@link #getStartupWarning()} instead of being shown here.
+ *
+ * The most important property of this logic is what it does not do: an
+ * installation with no or exactly one profile is resolved without asking anything and
+ * without touching a single file, so a single operator start stays exactly as fast and
+ * as quiet as it was before profiles existed.
+ */
+public class OperatorProfileBootstrap {
+
+ private String startupWarning;
+
+ /**
+ * Resolves the operator profile to start with.
+ *
+ * @param store registry to read the profiles from
+ * @param commandLineOptions parsed command line options
+ * @param choiceRequester requester used when the operator has to choose
+ * @return the resolved selection, or null when the operator chose to quit
+ */
+ public OperatorProfileSelection resolveAtStartup(final OperatorProfileStore store,
+ final CommandLineOptions commandLineOptions,
+ final OperatorProfileChoiceRequester choiceRequester) {
+
+ startupWarning = null;
+
+ List availableProfiles = store.loadProfiles();
+ String requestedProfileName = commandLineOptions == null
+ ? null
+ : commandLineOptions.getRequestedProfileName();
+
+ if (requestedProfileName != null) {
+ OperatorProfile requestedProfile = findProfile(availableProfiles, requestedProfileName);
+
+ if (requestedProfile != null) {
+ return OperatorProfilePaths.resolve(requestedProfile);
+ }
+
+ startupWarning = "The operator profile \"" + requestedProfileName
+ + "\" is unknown. KST4Contest continues with the normal profile selection.";
+ }
+
+ if (availableProfiles.isEmpty()) {
+ // No registry at all: the historic flat installation is the only profile.
+ return OperatorProfilePaths.resolve(store.buildImplicitRootProfile());
+ }
+
+ if (availableProfiles.size() == 1) {
+ return OperatorProfilePaths.resolve(availableProfiles.get(0));
+ }
+
+ String preselectedProfileId = store.loadLastUsedProfileId().orElse(null);
+ Optional chosenProfile =
+ choiceRequester.requestProfileChoice(availableProfiles, preselectedProfileId);
+
+ return chosenProfile.map(OperatorProfilePaths::resolve).orElse(null);
+ }
+
+ /**
+ * Returns a message that should be shown to the operator after startup.
+ *
+ * @return the warning text, or null when startup was unremarkable
+ */
+ public String getStartupWarning() {
+ return startupWarning;
+ }
+
+ /**
+ * Finds a profile by identifier or display name, ignoring case.
+ *
+ * @param availableProfiles profiles to search
+ * @param requestedName identifier or display name entered by the operator
+ * @return the matching profile, or null
+ */
+ private static OperatorProfile findProfile(final List availableProfiles,
+ final String requestedName) {
+
+ for (OperatorProfile currentProfile : availableProfiles) {
+ if (requestedName.equalsIgnoreCase(currentProfile.getProfileId())) {
+ return currentProfile;
+ }
+ }
+
+ for (OperatorProfile currentProfile : availableProfiles) {
+ if (requestedName.equalsIgnoreCase(currentProfile.getDisplayName())) {
+ return currentProfile;
+ }
+ }
+
+ return null;
+ }
+}
diff --git a/src/main/java/kst4contest/view/OperatorProfileChoiceRequester.java b/src/main/java/kst4contest/view/OperatorProfileChoiceRequester.java
new file mode 100644
index 00000000..1f026cca
--- /dev/null
+++ b/src/main/java/kst4contest/view/OperatorProfileChoiceRequester.java
@@ -0,0 +1,26 @@
+package kst4contest.view;
+
+import kst4contest.model.OperatorProfile;
+
+import java.util.List;
+import java.util.Optional;
+
+/**
+ * Asks the operator which profile to start with.
+ *
+ * The startup logic depends on this interface rather than on a dialog, so the
+ * decision which profile to use can be tested without a JavaFX runtime.
+ */
+@FunctionalInterface
+public interface OperatorProfileChoiceRequester {
+
+ /**
+ * Requests a profile choice.
+ *
+ * @param selectableProfiles profiles to choose from, never empty
+ * @param preselectedProfileId identifier to preselect, may be null
+ * @return the chosen profile, or empty when the operator wants to quit
+ */
+ Optional requestProfileChoice(List selectableProfiles,
+ String preselectedProfileId);
+}
diff --git a/src/main/java/kst4contest/view/OperatorProfilePickerDialog.java b/src/main/java/kst4contest/view/OperatorProfilePickerDialog.java
new file mode 100644
index 00000000..d9710159
--- /dev/null
+++ b/src/main/java/kst4contest/view/OperatorProfilePickerDialog.java
@@ -0,0 +1,143 @@
+package kst4contest.view;
+
+import kst4contest.model.OperatorProfile;
+
+import javafx.geometry.Insets;
+import javafx.scene.Scene;
+import javafx.scene.control.Button;
+import javafx.scene.control.Label;
+import javafx.scene.control.ListCell;
+import javafx.scene.control.ListView;
+import javafx.scene.input.KeyCode;
+import javafx.scene.input.MouseButton;
+import javafx.scene.layout.HBox;
+import javafx.scene.layout.Priority;
+import javafx.scene.layout.VBox;
+import javafx.stage.Modality;
+import javafx.stage.Stage;
+
+import java.util.List;
+import java.util.Optional;
+
+/**
+ * Asks the operator which profile to start with.
+ *
+ * The dialog is shown only when more than one profile exists. It is intentionally
+ * minimal, because it stands between the operator and a contest: the last used profile
+ * is preselected, the list has the focus, and Enter or a double click start immediately.
+ */
+public final class OperatorProfilePickerDialog {
+
+ private OperatorProfilePickerDialog() {
+ // Utility class.
+ }
+
+ /**
+ * Shows the picker and waits for the operator's choice.
+ *
+ * @param selectableProfiles profiles to choose from
+ * @param preselectedProfileId identifier of the profile to preselect, may be null
+ * @return the chosen profile, or empty when the operator wants to quit
+ */
+ public static Optional showAndSelect(final List selectableProfiles,
+ final String preselectedProfileId) {
+
+ Stage dialogStage = new Stage();
+ GuiUtils.applyApplicationIcon(dialogStage);
+ dialogStage.initModality(Modality.APPLICATION_MODAL);
+ dialogStage.setTitle("Select operator profile");
+
+ ListView profileListView = new ListView<>();
+ profileListView.getItems().addAll(selectableProfiles);
+ profileListView.setCellFactory(listView -> new OperatorProfileListCell());
+ VBox.setVgrow(profileListView, Priority.ALWAYS);
+
+ selectPreselectedProfile(profileListView, selectableProfiles, preselectedProfileId);
+
+ OperatorProfile[] chosenProfile = new OperatorProfile[1];
+
+ Button startButton = new Button("Start");
+ startButton.setDefaultButton(true);
+ startButton.setOnAction(event -> {
+ chosenProfile[0] = profileListView.getSelectionModel().getSelectedItem();
+ dialogStage.close();
+ });
+
+ Button quitButton = new Button("Quit");
+ quitButton.setCancelButton(true);
+ quitButton.setOnAction(event -> {
+ chosenProfile[0] = null;
+ dialogStage.close();
+ });
+
+ profileListView.setOnMouseClicked(event -> {
+ if (event.getButton() == MouseButton.PRIMARY && event.getClickCount() == 2) {
+ startButton.fire();
+ }
+ });
+
+ profileListView.setOnKeyPressed(event -> {
+ if (event.getCode() == KeyCode.ENTER) {
+ startButton.fire();
+ }
+ });
+
+ HBox buttonRow = new HBox(10, startButton, quitButton);
+ buttonRow.setPadding(new Insets(10, 0, 0, 0));
+
+ VBox dialogContent = new VBox(8,
+ new Label("More than one operator profile is configured."),
+ profileListView,
+ buttonRow);
+ dialogContent.setPadding(new Insets(15));
+
+ dialogStage.setScene(new Scene(dialogContent, 380, 280));
+ profileListView.requestFocus();
+ dialogStage.showAndWait();
+
+ return Optional.ofNullable(chosenProfile[0]);
+ }
+
+ private static void selectPreselectedProfile(final ListView profileListView,
+ final List selectableProfiles,
+ final String preselectedProfileId) {
+
+ int profileIndexToSelect = 0;
+
+ if (preselectedProfileId != null) {
+ for (int profileIndex = 0; profileIndex < selectableProfiles.size(); profileIndex++) {
+ if (preselectedProfileId.equalsIgnoreCase(
+ selectableProfiles.get(profileIndex).getProfileId())) {
+ profileIndexToSelect = profileIndex;
+ break;
+ }
+ }
+ }
+
+ profileListView.getSelectionModel().select(profileIndexToSelect);
+ profileListView.scrollTo(profileIndexToSelect);
+ }
+
+ /**
+ * Renders a profile with its name and the kind of worked data it uses.
+ */
+ private static final class OperatorProfileListCell extends ListCell {
+
+ @Override
+ protected void updateItem(final OperatorProfile profile, final boolean empty) {
+
+ super.updateItem(profile, empty);
+
+ if (empty || profile == null) {
+ setText(null);
+ return;
+ }
+
+ String workedDataDescription = profile.isRootProfile() || profile.isSharedWorkedDatabase()
+ ? "shared station worked database"
+ : "own worked database";
+
+ setText(profile.getDisplayName() + "\n" + workedDataDescription);
+ }
+ }
+}
diff --git a/src/test/java/kst4contest/view/CommandLineOptionsTest.java b/src/test/java/kst4contest/view/CommandLineOptionsTest.java
new file mode 100644
index 00000000..e9256c02
--- /dev/null
+++ b/src/test/java/kst4contest/view/CommandLineOptionsTest.java
@@ -0,0 +1,46 @@
+package kst4contest.view;
+
+import org.junit.jupiter.api.Test;
+
+import java.util.List;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNull;
+
+class CommandLineOptionsTest {
+
+ @Test
+ void profileArgumentIsAcceptedInBothSpellings() {
+ assertEquals("OP2", CommandLineOptions.parse(List.of("--profile=OP2")).getRequestedProfileName());
+ assertEquals("OP2", CommandLineOptions.parse(List.of("--profile", "OP2")).getRequestedProfileName());
+ assertEquals("DM5M Contest",
+ CommandLineOptions.parse(List.of("--profile", "DM5M Contest")).getRequestedProfileName());
+ }
+
+ @Test
+ void missingOrEmptyProfileArgumentsAreTreatedAsAbsent() {
+ assertNull(CommandLineOptions.parse(List.of()).getRequestedProfileName());
+ assertNull(CommandLineOptions.parse(null).getRequestedProfileName());
+ assertNull(CommandLineOptions.parse(List.of("--profile=")).getRequestedProfileName());
+ assertNull(CommandLineOptions.parse(List.of("--profile")).getRequestedProfileName());
+ }
+
+ @Test
+ void unrelatedArgumentsAreIgnoredInsteadOfFailing() {
+ assertEquals("OP2",
+ CommandLineOptions.parse(List.of("--verbose", "--profile=OP2", "somefile.adi"))
+ .getRequestedProfileName());
+ assertNull(CommandLineOptions.parse(List.of("--verbose", "-x")).getRequestedProfileName());
+ }
+
+ @Test
+ void rememberedOptionsDefaultToEmptyInsteadOfNull() {
+ CommandLineOptions.remember(null);
+ assertNull(CommandLineOptions.remembered().getRequestedProfileName());
+
+ CommandLineOptions.remember(new CommandLineOptions("OP2"));
+ assertEquals("OP2", CommandLineOptions.remembered().getRequestedProfileName());
+
+ CommandLineOptions.remember(null);
+ }
+}
diff --git a/src/test/java/kst4contest/view/OperatorProfileBootstrapTest.java b/src/test/java/kst4contest/view/OperatorProfileBootstrapTest.java
new file mode 100644
index 00000000..908a68c9
--- /dev/null
+++ b/src/test/java/kst4contest/view/OperatorProfileBootstrapTest.java
@@ -0,0 +1,161 @@
+package kst4contest.view;
+
+import kst4contest.controller.OperatorProfilePaths;
+import kst4contest.controller.OperatorProfileStore;
+import kst4contest.model.OperatorProfile;
+import kst4contest.model.OperatorProfileSelection;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.io.TempDir;
+
+import java.nio.file.Path;
+import java.util.List;
+import java.util.Optional;
+import java.util.concurrent.atomic.AtomicInteger;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertNull;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+class OperatorProfileBootstrapTest {
+
+ @TempDir
+ Path temporaryDirectory;
+
+ @Test
+ void installationWithoutRegistryStartsSilentlyOnTheHistoricLayout() {
+
+ AtomicInteger pickerInvocations = new AtomicInteger();
+ OperatorProfileStore store = storeAt();
+
+ OperatorProfileSelection resolved = new OperatorProfileBootstrap().resolveAtStartup(
+ store, new CommandLineOptions(null), countingPicker(pickerInvocations, null));
+
+ assertNotNull(resolved);
+ assertEquals("preferences.xml", resolved.getPreferencesRelativeFileName());
+ assertEquals("praktiKST.db", resolved.getWorkedDatabaseRelativeFileName());
+
+ // Nothing may be asked, and nothing may be written.
+ assertEquals(0, pickerInvocations.get());
+ assertTrue(store.loadProfiles().isEmpty());
+ }
+
+ @Test
+ void singleProfileStartsWithoutAskingAnything() {
+
+ AtomicInteger pickerInvocations = new AtomicInteger();
+ OperatorProfileStore store = storeAt();
+ store.saveProfiles(List.of(OperatorProfilePaths.buildRootProfile("Default")), "default");
+
+ OperatorProfileSelection resolved = new OperatorProfileBootstrap().resolveAtStartup(
+ store, new CommandLineOptions(null), countingPicker(pickerInvocations, null));
+
+ assertNotNull(resolved);
+ assertEquals(0, pickerInvocations.get());
+ }
+
+ @Test
+ void twoProfilesAskTheOperatorAndPreselectTheLastUsedOne() {
+
+ OperatorProfileStore store = storeAt();
+ OperatorProfile secondProfile = new OperatorProfile("OP2", "DN9APW", false, false);
+ store.saveProfiles(
+ List.of(OperatorProfilePaths.buildRootProfile("Default"), secondProfile), "OP2");
+
+ AtomicInteger pickerInvocations = new AtomicInteger();
+ String[] observedPreselection = new String[1];
+
+ OperatorProfileSelection resolved = new OperatorProfileBootstrap().resolveAtStartup(
+ store,
+ new CommandLineOptions(null),
+ (profiles, preselectedProfileId) -> {
+ pickerInvocations.incrementAndGet();
+ observedPreselection[0] = preselectedProfileId;
+ return Optional.of(profiles.get(1));
+ });
+
+ assertEquals(1, pickerInvocations.get());
+ assertEquals("OP2", observedPreselection[0]);
+ assertNotNull(resolved);
+ assertEquals("profiles/OP2/praktiKST.db", resolved.getWorkedDatabaseRelativeFileName());
+ }
+
+ @Test
+ void aValidProfileArgumentSkipsThePicker() {
+
+ OperatorProfileStore store = storeAt();
+ store.saveProfiles(
+ List.of(OperatorProfilePaths.buildRootProfile("Default"),
+ new OperatorProfile("OP2", "DN9APW", false, false)),
+ "default");
+
+ AtomicInteger pickerInvocations = new AtomicInteger();
+ OperatorProfileBootstrap bootstrap = new OperatorProfileBootstrap();
+
+ OperatorProfileSelection byId = bootstrap.resolveAtStartup(
+ store, new CommandLineOptions("op2"), countingPicker(pickerInvocations, null));
+
+ assertEquals(0, pickerInvocations.get());
+ assertEquals("profiles/OP2/preferences.xml", byId.getPreferencesRelativeFileName());
+ assertNull(bootstrap.getStartupWarning());
+
+ OperatorProfileSelection byDisplayName = bootstrap.resolveAtStartup(
+ store, new CommandLineOptions("DN9APW"), countingPicker(pickerInvocations, null));
+
+ assertEquals(0, pickerInvocations.get());
+ assertEquals("profiles/OP2/preferences.xml", byDisplayName.getPreferencesRelativeFileName());
+ }
+
+ @Test
+ void anUnknownProfileArgumentWarnsAndFallsBackToTheNormalSelection() {
+
+ OperatorProfileStore store = storeAt();
+ store.saveProfiles(
+ List.of(OperatorProfilePaths.buildRootProfile("Default"),
+ new OperatorProfile("OP2", "DN9APW", false, false)),
+ "default");
+
+ AtomicInteger pickerInvocations = new AtomicInteger();
+ OperatorProfileBootstrap bootstrap = new OperatorProfileBootstrap();
+
+ OperatorProfileSelection resolved = bootstrap.resolveAtStartup(
+ store, new CommandLineOptions("NOPE"), countingPicker(pickerInvocations, 0));
+
+ assertEquals(1, pickerInvocations.get());
+ assertNotNull(resolved);
+ assertNotNull(bootstrap.getStartupWarning());
+ assertTrue(bootstrap.getStartupWarning().contains("NOPE"));
+ }
+
+ @Test
+ void quittingInThePickerYieldsNoSelection() {
+
+ OperatorProfileStore store = storeAt();
+ store.saveProfiles(
+ List.of(OperatorProfilePaths.buildRootProfile("Default"),
+ new OperatorProfile("OP2", "DN9APW", false, false)),
+ "default");
+
+ OperatorProfileSelection resolved = new OperatorProfileBootstrap().resolveAtStartup(
+ store, new CommandLineOptions(null), (profiles, preselected) -> Optional.empty());
+
+ assertNull(resolved);
+ }
+
+ private OperatorProfileChoiceRequester countingPicker(final AtomicInteger invocationCounter,
+ final Integer profileIndexToChoose) {
+ return (profiles, preselectedProfileId) -> {
+ invocationCounter.incrementAndGet();
+
+ if (profileIndexToChoose == null) {
+ return Optional.of(profiles.get(0));
+ }
+
+ return Optional.of(profiles.get(profileIndexToChoose));
+ };
+ }
+
+ private OperatorProfileStore storeAt() {
+ return new OperatorProfileStore(temporaryDirectory.resolve("profiles.xml").toString());
+ }
+}