diff --git a/app/pom.xml b/app/pom.xml index 5767a0843..4837766a4 100644 --- a/app/pom.xml +++ b/app/pom.xml @@ -25,6 +25,13 @@ kit 17.0.0 + + + junit + junit + 4.13.2 + test + diff --git a/app/src/main/java/com/oracle/javafx/scenebuilder/app/SceneBuilderApp.java b/app/src/main/java/com/oracle/javafx/scenebuilder/app/SceneBuilderApp.java index e522c8276..e549d8739 100644 --- a/app/src/main/java/com/oracle/javafx/scenebuilder/app/SceneBuilderApp.java +++ b/app/src/main/java/com/oracle/javafx/scenebuilder/app/SceneBuilderApp.java @@ -42,6 +42,7 @@ import com.oracle.javafx.scenebuilder.app.i18n.I18N; import com.oracle.javafx.scenebuilder.app.menubar.MenuBarController; import com.oracle.javafx.scenebuilder.app.preferences.PreferencesController; +import com.oracle.javafx.scenebuilder.app.preferences.PreferencesImporter; import com.oracle.javafx.scenebuilder.app.preferences.PreferencesRecordGlobal; import com.oracle.javafx.scenebuilder.app.preferences.PreferencesWindowController; import com.oracle.javafx.scenebuilder.app.registration.RegistrationWindowController; @@ -388,6 +389,9 @@ public void handleLaunch(List files) { setApplicationUncaughtExceptionHandler(); + PreferencesImporter prefsImporter = PreferencesController.getSingleton().getImporter(); + prefsImporter.askForActionAndRun(); + MavenPreferences mavenPreferences = PreferencesController.getSingleton().getMavenPreferences(); // Creates the user library userLibrary = new UserLibrary(AppPlatform.getUserLibraryFolder(), @@ -792,9 +796,7 @@ private void performExit() { } } - private enum ACTION {START, STOP} - - ; + private enum ACTION {START, STOP}; private void logTimestamp(ACTION type) { switch (type) { diff --git a/app/src/main/java/com/oracle/javafx/scenebuilder/app/preferences/AppVersion.java b/app/src/main/java/com/oracle/javafx/scenebuilder/app/preferences/AppVersion.java new file mode 100644 index 000000000..4ffbb0ffc --- /dev/null +++ b/app/src/main/java/com/oracle/javafx/scenebuilder/app/preferences/AppVersion.java @@ -0,0 +1,159 @@ +/* + * Copyright (c) 2022, Gluon and/or its affiliates. + * All rights reserved. Use is subject to license terms. + * + * This file is available and licensed under the following license: + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions + * are met: + * + * - Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * - Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in + * the documentation and/or other materials provided with the distribution. + * - Neither the name of Oracle Corporation and Gluon nor the names of its + * contributors may be used to endorse or promote products derived + * from this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT + * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT + * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, + * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY + * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ +package com.oracle.javafx.scenebuilder.app.preferences; + +import java.util.Comparator; +import java.util.Optional; + +/** + * A record type to work with Scene Builder versions following the semantic + * versioning schema. Patch versions can be null and will be ignored then. + * + * Version numbers can be sorted, where major beats minor beats patch versions. + * Version numbers with patch versions will be considered as higher (or newer) than version numbers without patch numbers. + * This is regardless if there is no patch number or patch version is 0. Patch version 0 is higher than no patch version at all. + */ +public record AppVersion(int major, int minor, Integer patch) implements Comparable { + + /** + * @return {@link Comparator} sorting {@link AppVersion} instances in descending order. + */ + public static Comparator descending() { + return (a,b)->b.compareTo(a); + } + + public AppVersion(int major, int minor) { + this(major, minor, null); + } + + @Override + public int compareTo(AppVersion o) { + int majorDiff = major - o.major; + int minorDiff = minor - o.minor; + int patchDiff = calcPatchDiff(o); + if (majorDiff == 0) { + if (minorDiff == 0) { + return patchDiff; + } + return minorDiff; + } + return majorDiff; + } + + @Override + public String toString() { + StringBuilder builder = new StringBuilder(); + builder.append(major); + builder.append("."); + builder.append(minor); + if (patch != null) { + builder.append("."); + builder.append(patch); + } + return builder.toString(); + } + + /** + * Creates a version specific Scene Builder Preferences node name using the given prefix. + * @param prefix {@link String} used as prefix in Preferences node naming. + * @return + */ + public String nodeNameWithPrefix(String prefix) { + StringBuilder builder = new StringBuilder(); + if (prefix != null && !prefix.isBlank()) { + builder.append(prefix); + } + builder.append(major); + builder.append("."); + builder.append(minor); + if (patch != null) { + builder.append("."); + builder.append(patch); + } + return builder.toString(); + } + + protected int calcPatchDiff(AppVersion o) { + if (patch == null && o.patch == null) { + return 0; + } else if (patch == null && o.patch != null) { + return -1; + } else if (patch != null && o.patch == null) { + return 1; + } + return patch - o.patch; + } + + + /** + * Parses an optional AppVersion from any given String. + * Snapshot versions are not considered as valid. + * + * @param validVersion String representing a Scene Builder version. + * @return Empty optional when the String does not represent a valid version. If valid, then an optional AppVersion is returned. + */ + public static Optional fromString(String validVersion) { + String[] elements = validVersion.strip().split("[.]"); + return switch (elements.length) { + case 2 -> parseMajorMinor(elements); + case 3 -> parseMajorMinorPatch(elements); + default -> Optional.empty(); + }; + } + + private static Optional parseMajorMinor(String[] elements) { + try { + int major = Integer.parseInt(elements[0]); + int minor = Integer.parseInt(elements[1]); + if (major < 0 || minor < 0) { + return Optional.empty(); + } + return Optional.of(new AppVersion(major, minor)); + } catch (NumberFormatException nfe) { + return Optional.empty(); + } + } + + private static Optional parseMajorMinorPatch(String[] elements) { + try { + int major = Integer.parseInt(elements[0]); + int minor = Integer.parseInt(elements[1]); + int patch = Integer.parseInt(elements[2]); + if (major < 0 || minor < 0 || patch < 0) { + return Optional.empty(); + } + return Optional.of(new AppVersion(major, minor, patch)); + } catch (NumberFormatException nfe) { + return Optional.empty(); + } + } +} diff --git a/app/src/main/java/com/oracle/javafx/scenebuilder/app/preferences/PreferencesController.java b/app/src/main/java/com/oracle/javafx/scenebuilder/app/preferences/PreferencesController.java index e06a087d4..d464fb1e9 100644 --- a/app/src/main/java/com/oracle/javafx/scenebuilder/app/preferences/PreferencesController.java +++ b/app/src/main/java/com/oracle/javafx/scenebuilder/app/preferences/PreferencesController.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2016, 2017 Gluon and/or its affiliates. + * Copyright (c) 2016, 2012 Gluon and/or its affiliates. * Copyright (c) 2012, 2014, Oracle and/or its affiliates. * All rights reserved. Use is subject to license terms. * @@ -32,17 +32,20 @@ */ package com.oracle.javafx.scenebuilder.app.preferences; -import com.oracle.javafx.scenebuilder.app.DocumentWindowController; -import com.oracle.javafx.scenebuilder.app.util.AppSettings; -import com.oracle.javafx.scenebuilder.kit.preferences.PreferencesControllerBase; - import java.util.HashMap; import java.util.Map; +import java.util.Optional; import java.util.logging.Level; import java.util.logging.Logger; import java.util.prefs.BackingStoreException; import java.util.prefs.Preferences; +import com.oracle.javafx.scenebuilder.app.DocumentWindowController; +import com.oracle.javafx.scenebuilder.app.util.AppSettings; +import com.oracle.javafx.scenebuilder.kit.preferences.MavenPreferences; +import com.oracle.javafx.scenebuilder.kit.preferences.PreferencesControllerBase; +import com.oracle.javafx.scenebuilder.kit.preferences.RepositoryPreferences; + /** * Defines preferences for Scene Builder App. */ @@ -55,7 +58,8 @@ public class PreferencesController extends PreferencesControllerBase{ **************************************************************************/ // PREFERENCES NODE NAME - static final String SB_RELEASE_NODE = "SB_"+AppSettings.getSceneBuilderVersion(); //NOI18N + static final String SB_RELEASE_NODE_PREFIX = "SB_"; + static final String SB_RELEASE_NODE = SB_RELEASE_NODE_PREFIX+AppSettings.getSceneBuilderVersion(); //NOI18N // GLOBAL PREFERENCES static final String TOOL_THEME = "TOOL_THEME"; //NOI18N @@ -103,8 +107,8 @@ public class PreferencesController extends PreferencesControllerBase{ * * **************************************************************************/ - private PreferencesController() { - super(SB_RELEASE_NODE, new PreferencesRecordGlobal()); + private PreferencesController(Preferences rootNode) { + super(rootNode, SB_RELEASE_NODE, new PreferencesRecordGlobal()); // Cleanup document preferences at start time : final String items = applicationRootPreferences.get(RECENT_ITEMS, null); //NOI18N @@ -139,14 +143,31 @@ private PreferencesController() { **************************************************************************/ public static synchronized PreferencesController getSingleton() { + return getSingleton(null); + } + + /** + * This method allows to pass in a custom {@link Preferences} node, e.g. for testing. + * When configured with null, the Preferences node to be used will be defined internally. + * The custom node is only set with the first call, when the {@link PreferencesController} was initialized before, t + * the Preferences node to be used cannot be changed anymore. + * + * @param prefs {@link Preferences} node to be used, can be null. + * @return The one and only PreferencesController instance. + */ + protected static synchronized PreferencesController getSingleton(Preferences prefs) { if (singleton == null) { - singleton = new PreferencesController(); + singleton = new PreferencesController(prefs); singleton.getRecordGlobal().readFromJavaPreferences(); + } else { + if (prefs != null) { + Logger.getLogger(PreferencesController.class.getName()) + .log(Level.INFO, "PreferencesController was already initialized. Ignoring the provided preferences node."); + } } return singleton; } - public PreferencesRecordDocument getRecordDocument(final DocumentWindowController dwc) { final PreferencesRecordDocument recordDocument; if (recordDocuments.containsKey(dwc)) { @@ -185,4 +206,39 @@ public PreferencesRecordGlobal getRecordGlobal() { protected String getEffectiveUsedRootNode() { return applicationRootPreferences.absolutePath(); } + + /** + * If there were older versions of Scene Builder used, + * this will return the Preferences node matching the most recent previous version. + * @return Optional version number of + */ + protected Optional getPreviousVersionSettings() { + return new VersionedPreferencesFinder(SB_RELEASE_NODE_PREFIX, applicationPreferences) + .previousVersionPrefs(); + } + + /** + * This allows to re-initialize application settings after user has decided to + * import previous version settings. If not called after import, a restart of + * Scene Builder is needed to have all imported settings effective. + *

+ * Reloads the contents of {@link PreferencesRecordGlobal} and initializes + * {@link MavenPreferences} and {@link RepositoryPreferences} afterwards. + */ + protected void reload() { + getRecordGlobal().readFromJavaPreferences(); + initializeMavenPreferences(); + initializeRepositoryPreferences(); + } + + /** + * @return A {@link PreferencesImporter} instance for this application which + * will help to import previous version application settings. + */ + public PreferencesImporter getImporter() { + Optional previousVersionSettings = getPreviousVersionSettings(); + PreferencesImporter importer = new PreferencesImporter(applicationRootPreferences, previousVersionSettings); + importer.runAfterImport(this::reload); + return importer; + } } diff --git a/app/src/main/java/com/oracle/javafx/scenebuilder/app/preferences/PreferencesImporter.java b/app/src/main/java/com/oracle/javafx/scenebuilder/app/preferences/PreferencesImporter.java new file mode 100644 index 000000000..84a98e294 --- /dev/null +++ b/app/src/main/java/com/oracle/javafx/scenebuilder/app/preferences/PreferencesImporter.java @@ -0,0 +1,205 @@ +/* + * Copyright (c) 2022, Gluon and/or its affiliates. + * All rights reserved. Use is subject to license terms. + * + * This file is available and licensed under the following license: + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions + * are met: + * + * - Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * - Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in + * the documentation and/or other materials provided with the distribution. + * - Neither the name of Oracle Corporation and Gluon nor the names of its + * contributors may be used to endorse or promote products derived + * from this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT + * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT + * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, + * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY + * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ +package com.oracle.javafx.scenebuilder.app.preferences; + +import java.time.LocalDateTime; +import java.util.Objects; +import java.util.Optional; +import java.util.logging.Level; +import java.util.logging.Logger; +import java.util.prefs.BackingStoreException; +import java.util.prefs.Preferences; + +import com.oracle.javafx.scenebuilder.kit.alert.SBAlert; + +import javafx.scene.control.Alert; +import javafx.scene.control.Alert.AlertType; +import javafx.scene.control.ButtonType; + +/** + * Imports all keys and children (including keys) from an arbitrary + * {@link Preferences} node into the predefined applicationPreferences node. + */ +public class PreferencesImporter { + + /*************************************************************************** + * * + * Static fields * + * * + **************************************************************************/ + + // GLOBAL PREFERENCES + static final String ASKED_FOR_IMPORT = "ASKED_FOR_IMPORT"; + + /*************************************************************************** + * * + * Instance fields * + * * + **************************************************************************/ + + private final Logger logger = Logger.getLogger(PreferencesImporter.class.getName()); + private final Preferences target; + private final Optional optionalSourceNode; + private Runnable actionAfterImport; + /** + * Creates a new Preferences importer. + * + * @param applicationPreferences Scene Builder {@link Preferences} node, + * determines where all settings shall be imported + * into. + * @param optionalSourceNode {@link VersionedPreferences} as a possible candidate to import settings from + */ + public PreferencesImporter(Preferences applicationPreferences, Optional optionalSourceNode) { + this.target = Objects.requireNonNull(applicationPreferences); + this.optionalSourceNode = Objects.requireNonNull(optionalSourceNode); + this.actionAfterImport = ()->logger.log(Level.INFO, "Importing settings completed. No post-import action defined."); + } + + /** + * Imports preferences from an existing node of an older Scene Builder version + * to the corresponding node of the current Scene Builder version. + * + * @param oldVersion {@link AppVersion} Version number so that, if needed, + * the import process can be customized depending on the + * version number. + * @param importSourceNode {@link Preferences} node of the previous version of + * Scene Builder. + * + * @throws BackingStoreException in case of problems accessing the Preferences + * store. + */ + protected void importFrom(Preferences importSourceNode) throws BackingStoreException { + copyChildren(importSourceNode,target); + } + + protected void importFrom(VersionedPreferences importSourceNode) throws BackingStoreException { + importFrom(importSourceNode.node()); + } + + + private void copyChildren(Preferences source, Preferences targetNode) throws BackingStoreException { + logger.log(Level.INFO, String.format("from node %s", source.name())); + String[] keys = source.keys(); + for (String key : keys) { + String value = source.get(key, null); + if (value != null) { + targetNode.put(key, value); + } + } + + String[] children = source.childrenNames(); + for (String child : children) { + copyChildren(source.node(child), targetNode.node(child)); + } + } + + /** + * Decides if the user shall be asked to import settings of previous Scene + * Builder version. Once the user made a decision, the preferences key + * ASKED_FOR_IMPORT is set, then this method will return false. + * + * @return true in case the import decision was not yet made. + */ + public boolean askForImport() { + String lastTimeAsked = target.get(ASKED_FOR_IMPORT, null); + return lastTimeAsked == null; + } + + /** + * Stores the preferences key ASKED_FOR_IMPORT upon request. If this key does + * not exist, Scene Builder will ask user to import settings from previous + * version during application startup. + */ + public void saveTimestampWhenAskedForImport() { + target.put(ASKED_FOR_IMPORT, LocalDateTime.now().toString()); + } + + /** + * Attempts to import settings of a previous version if existing. + * There is no user feedback in case of error. + */ + public void tryImportingPreviousVersionSettings() { + if (this.optionalSourceNode.isPresent()) { + VersionedPreferences source = this.optionalSourceNode.get(); + try { + importFrom(source); + } catch (Exception importError) { + logger.log(Level.SEVERE, String.format("Error during preferences import!", importError)); + } + try { + this.actionAfterImport.run(); + } catch (Exception postImportActionError) { + logger.log(Level.SEVERE, String.format("Error while running post-import action!", postImportActionError)); + } + saveTimestampWhenAskedForImport(); + } + } + + /** + * Decides if the user shall be asked to import settings of previous Scene + * Builder version but ONLY when settings of a previous version have been found. + * + * @return true when previous version settings have been found and user has not yet decided + */ + public boolean askForImportIfOlderSettingsExist() { + return this.optionalSourceNode.isPresent() && askForImport(); + } + + /** + * Defines an activity which will be executed after import of settings. + * + * @param action {@link Runnable} + * @throws NullPointerException when action is null + */ + public void runAfterImport(Runnable action) { + this.actionAfterImport = Objects.requireNonNull(action); + } + + /** + * Will raise a JavaFX {@link Alert} to ask the user whether to import previous version settings or not. + * The question will only appear in cases where previous version settings exist and the user decision has not been saved yet. + */ + public void askForActionAndRun() { + if (askForImportIfOlderSettingsExist()) { + SBAlert customAlert = new SBAlert(AlertType.CONFIRMATION, ButtonType.YES, ButtonType.NO); + customAlert.initOwner(null); + customAlert.setTitle("Gluon Scene Builder"); + customAlert.setHeaderText("Import settings"); + customAlert.setContentText("Previous version settings found.\nDo you want to import those?\n\nScene Builder will remember your decision and not ask again."); + Optional response = customAlert.showAndWait(); + System.out.println(response); + if (response.isPresent() && ButtonType.YES.equals(response.get())) { + tryImportingPreviousVersionSettings(); + } + } + } +} diff --git a/app/src/main/java/com/oracle/javafx/scenebuilder/app/preferences/VersionedPreferences.java b/app/src/main/java/com/oracle/javafx/scenebuilder/app/preferences/VersionedPreferences.java new file mode 100644 index 000000000..69c82bb3d --- /dev/null +++ b/app/src/main/java/com/oracle/javafx/scenebuilder/app/preferences/VersionedPreferences.java @@ -0,0 +1,42 @@ +/* + * Copyright (c) 2022, Gluon and/or its affiliates. + * All rights reserved. Use is subject to license terms. + * + * This file is available and licensed under the following license: + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions + * are met: + * + * - Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * - Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in + * the documentation and/or other materials provided with the distribution. + * - Neither the name of Oracle Corporation and Gluon nor the names of its + * contributors may be used to endorse or promote products derived + * from this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT + * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT + * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, + * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY + * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ +package com.oracle.javafx.scenebuilder.app.preferences; + +import java.util.prefs.Preferences; + +/** + * Assigns an {@link AppVersion} to a given Preferences node. + * This will help with backward compatibility when structure of SB Preferences changes. + */ +public record VersionedPreferences(AppVersion version, Preferences node) { + +} diff --git a/app/src/main/java/com/oracle/javafx/scenebuilder/app/preferences/VersionedPreferencesFinder.java b/app/src/main/java/com/oracle/javafx/scenebuilder/app/preferences/VersionedPreferencesFinder.java new file mode 100644 index 000000000..646cdb263 --- /dev/null +++ b/app/src/main/java/com/oracle/javafx/scenebuilder/app/preferences/VersionedPreferencesFinder.java @@ -0,0 +1,131 @@ +/* + * Copyright (c) 2021, Gluon and/or its affiliates. + * All rights reserved. Use is subject to license terms. + * + * This file is available and licensed under the following license: + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions + * are met: + * + * - Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * - Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in + * the documentation and/or other materials provided with the distribution. + * - Neither the name of Oracle Corporation and Gluon nor the names of its + * contributors may be used to endorse or promote products derived + * from this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT + * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT + * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, + * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY + * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ +package com.oracle.javafx.scenebuilder.app.preferences; + +import java.util.Arrays; +import java.util.Collections; +import java.util.List; +import java.util.Objects; +import java.util.Optional; +import java.util.logging.Level; +import java.util.logging.Logger; +import java.util.prefs.BackingStoreException; +import java.util.prefs.Preferences; + +import com.oracle.javafx.scenebuilder.app.util.AppSettings; + +/** + * A utility which searches for version specific {@link Preferences} nodes belonging to Scene Builder. + * + */ +final class VersionedPreferencesFinder { + + private final Preferences applicationPreferences; + + private final String nodePrefix; + + public VersionedPreferencesFinder(String nodePrefix, Preferences rootNode) { + this.applicationPreferences = Objects.requireNonNull(rootNode); + this.nodePrefix = Objects.requireNonNull(nodePrefix); + } + + /** + * Collects the versions where application settings for Scene Builder exists into a list, starting with the most recent version. + * In case of errors while accessing the Preferences store, an empty list is returned. + * @return List with {@link AppVersion} starting with most recent version as first element. + */ + public List getDetectedVersions() { + try { + String[] children = applicationPreferences.childrenNames(); + return Arrays.stream(children) + .map(this::removePrefixFromVersionedNode) + .map(AppVersion::fromString) + .filter(Optional::isPresent) + .map(Optional::get) + .sorted(AppVersion.descending()) + .toList(); + } catch (BackingStoreException ex) { + Logger.getLogger(PreferencesController.class.getName()).log(Level.SEVERE, + "Failed to detect possibly existing settings of other versions!", ex); + } + return Collections.emptyList(); + } + + private String removePrefixFromVersionedNode(String nodeName) { + if (nodeName.startsWith(nodePrefix)) { + return nodeName.substring(nodePrefix.length()); + } + return nodeName; + } + + /** + * Provides the list of all preferences nodes with a version assigned to. + * Will not work with snapshot versions. + * + * @return List of preferences nodes (as {@link VersionedPreferences}) belonging to an older version of Scene Builder + */ + public List getPreviousVersions() { + var currentVersion = AppVersion.fromString(AppSettings.getSceneBuilderVersion()); + if (currentVersion.isPresent()) { + var current = currentVersion.get(); + return getDetectedVersions().stream() + .filter(other->other.compareTo(current) < 0) + .sorted(AppVersion.descending()) + .map(this::buildNode) + .toList(); + } + return Collections.emptyList(); + } + + /** + * If an older version of Scene Builder was used, there might be a preferences + * node for this older version. If this node exists, the optional will carry a + * {@link VersionedPreferences} providing the {@link AppVersion} and the + * {@link Preferences} node. + * + * @return Optional a {@link VersionedPreferences} in case an older version of Scene + * Builder was installed. When there are no settings, the optional will + * be empty. + */ + public Optional previousVersionPrefs() { + List previousVersions = getPreviousVersions(); + if (previousVersions.isEmpty()) { + return Optional.empty(); + } + return Optional.of(previousVersions.get(0)); + } + + private VersionedPreferences buildNode(AppVersion version) { + String node = version.nodeNameWithPrefix(nodePrefix); + return new VersionedPreferences(version, applicationPreferences.node(node)); + }; +} diff --git a/app/src/test/java/com/oracle/javafx/scenebuilder/app/preferences/AppVersionTest.java b/app/src/test/java/com/oracle/javafx/scenebuilder/app/preferences/AppVersionTest.java new file mode 100644 index 000000000..7992a9266 --- /dev/null +++ b/app/src/test/java/com/oracle/javafx/scenebuilder/app/preferences/AppVersionTest.java @@ -0,0 +1,159 @@ +/* + * Copyright (c) 2022, Gluon and/or its affiliates. + * All rights reserved. Use is subject to license terms. + * + * This file is available and licensed under the following license: + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions + * are met: + * + * - Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * - Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in + * the documentation and/or other materials provided with the distribution. + * - Neither the name of Oracle Corporation and Gluon nor the names of its + * contributors may be used to endorse or promote products derived + * from this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT + * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT + * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, + * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY + * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ +package com.oracle.javafx.scenebuilder.app.preferences; + +import static org.junit.Assert.*; + +import java.util.List; +import java.util.Optional; + +import org.junit.Test; + +public class AppVersionTest { + + @Test + public void that_comparision_yields_0_in_case_of_equal_versions() { + AppVersion old = new AppVersion(8, 5, 0); + AppVersion moreRecent = new AppVersion(8, 5, 0); + int result = moreRecent.compareTo(old); + assertTrue(result == 0); + } + + @Test + public void that_new_major_version_is_larger_than_older_major_version() { + AppVersion old = new AppVersion(8, 5, 0); + AppVersion moreRecent = new AppVersion(16, 0, 1); + int result = moreRecent.compareTo(old); + assertTrue(result > 0); + } + + @Test + public void that_minor_version_is_used_when_major_is_equal() { + AppVersion old = new AppVersion(16, 1, 0); + AppVersion moreRecent = new AppVersion(16, 2, 0); + int result = moreRecent.compareTo(old); + assertTrue(result > 0); + } + + @Test + public void that_patch_version_is_used_when_major_and_minor_are_equal() { + AppVersion old = new AppVersion(16, 0, 0); + AppVersion moreRecent = new AppVersion(16, 0, 1); + int result = moreRecent.compareTo(old); + assertTrue(result > 0); + } + + @Test + public void that_a_version_can_be_parsed_from_string() { + String validVersion = "42.12.12"; + AppVersion expected = new AppVersion(42, 12, 12); + Optional parsedVersion = AppVersion.fromString(validVersion); + assertTrue(parsedVersion.isPresent()); + assertEquals(expected, parsedVersion.get()); + } + + @Test + public void that_2digit_versions_are_parsed() { + assertTrue(AppVersion.fromString("2.0").isPresent()); + assertTrue(AppVersion.fromString("2.0.").isPresent()); + } + + @Test + public void that_useful_toString_exists() { + assertEquals("1.0", new AppVersion(1, 0).toString()); + assertEquals("1.2", new AppVersion(1, 2).toString()); + assertEquals("1.0.0", new AppVersion(1, 0, 0).toString()); + assertEquals("1.0.1", new AppVersion(1, 0, 1).toString()); + } + + @Test + public void that_empty_optionals_are_returned_from_illegal_version_strings() { + assertFalse(AppVersion.fromString("-1.-1.-1").isPresent()); + assertFalse(AppVersion.fromString("-1.10.10").isPresent()); + assertFalse(AppVersion.fromString("1.-10.-1").isPresent()); + assertFalse(AppVersion.fromString("1.1.-100").isPresent()); + assertFalse(AppVersion.fromString("-1.-1.-1").isPresent()); + assertFalse(AppVersion.fromString(".1.1.1.").isPresent()); + assertFalse(AppVersion.fromString("1..1").isPresent()); + assertFalse(AppVersion.fromString("11").isPresent()); + assertFalse(AppVersion.fromString("/t").isPresent()); + assertFalse(AppVersion.fromString(" . . ").isPresent()); + assertFalse(AppVersion.fromString("1.2.a").isPresent()); + assertFalse(AppVersion.fromString("-1.-2").isPresent()); + assertFalse(AppVersion.fromString("2.-1").isPresent()); + } + + @Test + public void that_node_name_is_created_with_prefix() { + assertEquals("SB_16.0.1", new AppVersion(16, 0, 1).nodeNameWithPrefix("SB_")); + assertEquals("TEST_16.0", new AppVersion(16, 0).nodeNameWithPrefix("TEST_")); + assertEquals("16.0", new AppVersion(16, 0).nodeNameWithPrefix("")); + assertEquals("16.0", new AppVersion(16, 0).nodeNameWithPrefix(null)); + } + + @Test + public void that_descending_sort_by_major_minor_patch_works() { + List versions = List.of( + new AppVersion(16, 0, 1), + new AppVersion(8, 5, 0), + new AppVersion(16, 0, 2), + new AppVersion(17, 0, 1), + new AppVersion(11, 2, 0), + new AppVersion(8, 5, 0), + new AppVersion(2, 0, 0), + new AppVersion(17, 0, 0), + new AppVersion(8, 5, 0), + new AppVersion(11, 1, 3), + new AppVersion(17, 2, 1), + new AppVersion(16, 0, 0), + new AppVersion(16, 0)); + + List sorted = versions.stream().sorted(AppVersion.descending()).toList(); + + List expectedOrder = List.of( + new AppVersion(17, 2, 1), + new AppVersion(17, 0, 1), + new AppVersion(17, 0, 0), + new AppVersion(16, 0, 2), + new AppVersion(16, 0, 1), + new AppVersion(16, 0, 0), + new AppVersion(16, 0), + new AppVersion(11, 2, 0), + new AppVersion(11, 1, 3), + new AppVersion(8, 5, 0), + new AppVersion(8, 5, 0), + new AppVersion(8, 5, 0), + new AppVersion(2, 0, 0)); + + assertEquals(expectedOrder, sorted); + } +} diff --git a/app/src/test/java/com/oracle/javafx/scenebuilder/app/preferences/PreferencesControllerTest.java b/app/src/test/java/com/oracle/javafx/scenebuilder/app/preferences/PreferencesControllerTest.java index 042a45b3a..5ce6dd403 100644 --- a/app/src/test/java/com/oracle/javafx/scenebuilder/app/preferences/PreferencesControllerTest.java +++ b/app/src/test/java/com/oracle/javafx/scenebuilder/app/preferences/PreferencesControllerTest.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2021, Gluon and/or its affiliates. + * Copyright (c) 2022, Gluon and/or its affiliates. * All rights reserved. Use is subject to license terms. * * This file is available and licensed under the following license: @@ -32,27 +32,120 @@ package com.oracle.javafx.scenebuilder.app.preferences; import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertTrue; +import java.util.List; +import java.util.Optional; +import java.util.prefs.Preferences; + +import org.junit.BeforeClass; import org.junit.Test; import com.oracle.javafx.scenebuilder.app.util.AppSettings; +import com.oracle.javafx.scenebuilder.kit.editor.panel.library.maven.repository.Repository; public class PreferencesControllerTest { + private static Preferences testNode; + + private static PreferencesController classUnderTest; + + @BeforeClass + public static void setup() { + testNode = Preferences.userRoot() + .node("SBTEST") + .node("com/oracle/javafx/scenebuilder/app/preferences"); + PrefsHelper.removeAllChildNodes(testNode); + classUnderTest = PreferencesController.getSingleton(testNode); + } + @Test public void that_preferences_are_stored_per_version() { String appVersion = AppSettings.getSceneBuilderVersion(); - String versionSpecificNode = "SB_"+appVersion; + String versionSpecificNode = "SB_" + appVersion; assertEquals(versionSpecificNode, PreferencesController.SB_RELEASE_NODE); } @Test public void that_prefs_root_node_is_version_specific() { - String prefsNodeUsed = PreferencesController.getSingleton() - .getEffectiveUsedRootNode(); + String prefsNodeUsed = classUnderTest.getEffectiveUsedRootNode(); String appVersion = AppSettings.getSceneBuilderVersion(); - String versionSpecificNode = "SB_"+appVersion; - String expectedPrefsNode = "/com/oracle/javafx/scenebuilder/app/preferences/"+versionSpecificNode; + String versionSpecificNode = "SB_" + appVersion; + String expectedPrefsNode = "/SBTEST/com/oracle/javafx/scenebuilder/app/preferences/" + versionSpecificNode; assertEquals(expectedPrefsNode, prefsNodeUsed); } + + @Test + public void that_most_recent_previous_version_is_detected() { + PrefsHelper.removeAllNonReleaseNodes(testNode); + testNode.node("SB_8.5").put("testkey", "testvalue"); + testNode.node("SB_2.0").put("testkey", "testvalue"); + Optional mostRecentPreviousVersion = classUnderTest.getPreviousVersionSettings(); + assertTrue(mostRecentPreviousVersion.isPresent()); + + assertEquals(new AppVersion(8, 5), mostRecentPreviousVersion.get().version()); + assertEquals("SB_8.5", mostRecentPreviousVersion.get().node().name()); + } + + @Test + public void that_no_version_is_detected_in_case_that_only_newer_versions_exist() { + PrefsHelper.removeAllNonReleaseNodes(testNode); + testNode.node("SB_199.199.199").put("testkey", "testvalue"); + Optional mostRecentPreviousVersion = classUnderTest.getPreviousVersionSettings(); + assertTrue(mostRecentPreviousVersion.isEmpty()); + } + + @Test + public void that_previous_version_settings_can_be_imported() { + PrefsHelper.removeAllNonReleaseNodes(testNode); + + // GIVEN + + testNode.node("SB_8.9.9").put("RECENT_ITEMS", "/folder/file.fxml"); + + Preferences mavenLib = testNode.node("SB_8.9.9").node("ARTIFACTS").node("org.name:library:0.0.1"); + mavenLib.put("path", "/location/of/file.jar"); + mavenLib.put("groupID", "org.name"); + mavenLib.put("filter", ""); + mavenLib.put("dependencies", ""); + mavenLib.put("artifactId", "library"); + mavenLib.put("version", "0.0.1"); + + Preferences repository = testNode.node("SB_8.9.9").node("REPOSITORIES").node("custom-repository"); + repository.put("ID", "custom-repository"); + repository.put("URL", "http://localhost/myrepo"); + repository.put("type", "default"); + repository.put("User", "username"); + repository.put("Password", "password"); + + + // WHEN + PreferencesImporter importer = classUnderTest.getImporter(); + importer.tryImportingPreviousVersionSettings(); + + // THEN + String appVersion = AppSettings.getSceneBuilderVersion(); + String versionSpecificNode = "SB_" + appVersion; + Preferences targetNode = testNode.node(versionSpecificNode); + + // User Decision is memorized + assertNotNull(targetNode.get(PreferencesImporter.ASKED_FOR_IMPORT, null)); + + // Maven Repositories are initialized properly + List artifacts = classUnderTest.getMavenPreferences().getArtifactsCoordinates(); + assertFalse(artifacts.isEmpty()); + assertTrue(artifacts.contains("org.name:library:0.0.1")); + + // User Repositories are properly initialized + List repositories = classUnderTest.getRepositoryPreferences().getRepositories(); + assertFalse(repositories.isEmpty()); + assertEquals("http://localhost/myrepo", repositories.get(0).getURL()); + + // Recent items + List recentItems = classUnderTest.getRecordGlobal().getRecentItems(); + assertFalse(recentItems.isEmpty()); + assertEquals("/folder/file.fxml", recentItems.get(0)); + } } diff --git a/app/src/test/java/com/oracle/javafx/scenebuilder/app/preferences/PreferencesImporterTest.java b/app/src/test/java/com/oracle/javafx/scenebuilder/app/preferences/PreferencesImporterTest.java new file mode 100644 index 000000000..abd5cdf32 --- /dev/null +++ b/app/src/test/java/com/oracle/javafx/scenebuilder/app/preferences/PreferencesImporterTest.java @@ -0,0 +1,108 @@ +package com.oracle.javafx.scenebuilder.app.preferences; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertTrue; + +import java.util.HashSet; +import java.util.Optional; +import java.util.Set; +import java.util.prefs.Preferences; + +import org.junit.AfterClass; +import org.junit.BeforeClass; +import org.junit.Test; + +public class PreferencesImporterTest { + + private PreferencesImporter classUnderTest; + + @BeforeClass + @AfterClass + public static void cleanUpPrefs() throws Exception { + Set nodesToBeRemoved = Set.of("SOURCE_TO_IMPORT", + "SB_TEST_TARGET", + "SB_TEST_TARGET2", + "SB_OLD_VER"); + for (String nodeName : nodesToBeRemoved) { + Preferences.userRoot().node(nodeName).removeNode(); + } + } + @Test + public void that_settings_are_copied_between_nodes() throws Exception { + Preferences mySourcePrefs = Preferences.userRoot().node("SOURCE_TO_IMPORT"); + mySourcePrefs.put("anykey", "anyvalue"); + mySourcePrefs.node("CHILD1").put("key1", "value1"); + mySourcePrefs.node("CHILD1").node("CHILD2").put("key2", "value2"); + mySourcePrefs.node("CHILD1").node("CHILD2").node("CHILD3").put("key3", "value3"); + mySourcePrefs.node("CHILD1").node("CHILD2").node("CHILD4"); + + Preferences myTargetPrefs = Preferences.userRoot().node("SB_TEST_TARGET"); + + classUnderTest = new PreferencesImporter(myTargetPrefs, Optional.empty()); + classUnderTest.importFrom(mySourcePrefs); + + assertEquals("anyvalue", myTargetPrefs.get("anykey", null)); + assertEquals("value1", myTargetPrefs.node("CHILD1").get("key1", null)); + assertEquals("value2", myTargetPrefs.node("CHILD1").node("CHILD2").get("key2", null)); + assertEquals("value3", myTargetPrefs.node("CHILD1").node("CHILD2").node("CHILD3").get("key3", null)); + } + + @Test + public void that_user_will_be_only_asked_when_import_decision_was_not_made() throws Exception { + Preferences appPrefs = Preferences.userRoot().node("SB_TEST_TARGET"); + classUnderTest = new PreferencesImporter(appPrefs, Optional.empty()); + + assertTrue(classUnderTest.askForImport()); + + classUnderTest.saveTimestampWhenAskedForImport(); + assertFalse(classUnderTest.askForImport()); + } + + @Test + public void that_user_will_be_only_asked_when_previous_version_settings_exist() throws Exception { + AppVersion oldVersion = new AppVersion(0, 9); + Preferences olderPrefs = Preferences.userRoot().node("SB_OLD_VER"); + Preferences appPrefs = Preferences.userRoot().node("SB_TEST_TARGET2"); + + classUnderTest = new PreferencesImporter(appPrefs, Optional.of(new VersionedPreferences(oldVersion, olderPrefs))); + assertTrue(classUnderTest.askForImportIfOlderSettingsExist()); + + classUnderTest = new PreferencesImporter(appPrefs, Optional.empty()); + assertFalse(classUnderTest.askForImportIfOlderSettingsExist()); + + classUnderTest = new PreferencesImporter(appPrefs, Optional.of(new VersionedPreferences(oldVersion, olderPrefs))); + classUnderTest.saveTimestampWhenAskedForImport(); + assertFalse(classUnderTest.askForImportIfOlderSettingsExist()); + } + + @Test + public void that_run_after_import_action_is_executed_in_tryImportingPreviousVersionSettings() { + Set responses = new HashSet<>(); + Runnable action = () -> responses.add("action performed"); + + AppVersion oldVersion = new AppVersion(0, 9); + Preferences olderPrefs = Preferences.userRoot().node("SB_OLD_VER"); + olderPrefs.put("somekey", "1234"); + + Preferences appPrefs = Preferences.userRoot().node("SB_TEST_TARGET2"); + Optional previousVersionSettings = Optional.of(new VersionedPreferences(oldVersion, olderPrefs)); + + classUnderTest = new PreferencesImporter(appPrefs, previousVersionSettings); + classUnderTest.runAfterImport(action); + classUnderTest.tryImportingPreviousVersionSettings(); + + assertTrue(responses.contains("action performed")); + assertEquals("1234", appPrefs.get("somekey", null)); + assertNotNull(appPrefs.get(PreferencesImporter.ASKED_FOR_IMPORT, null)); + } + + @Test(expected = NullPointerException.class) + public void that_null_value_for_run_after_import_action_is_not_accepted() { + Preferences appPrefs = Preferences.userRoot().node("SB_TEST_TARGET"); + classUnderTest = new PreferencesImporter(appPrefs, Optional.empty()); + classUnderTest.runAfterImport(null); + } + +} diff --git a/app/src/test/java/com/oracle/javafx/scenebuilder/app/preferences/PrefsHelper.java b/app/src/test/java/com/oracle/javafx/scenebuilder/app/preferences/PrefsHelper.java new file mode 100644 index 000000000..594d633b6 --- /dev/null +++ b/app/src/test/java/com/oracle/javafx/scenebuilder/app/preferences/PrefsHelper.java @@ -0,0 +1,73 @@ +/* + * Copyright (c) 2022, Gluon and/or its affiliates. + * All rights reserved. Use is subject to license terms. + * + * This file is available and licensed under the following license: + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions + * are met: + * + * - Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * - Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in + * the documentation and/or other materials provided with the distribution. + * - Neither the name of Oracle Corporation and Gluon nor the names of its + * contributors may be used to endorse or promote products derived + * from this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT + * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT + * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, + * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY + * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ +package com.oracle.javafx.scenebuilder.app.preferences; + +import java.util.prefs.BackingStoreException; +import java.util.prefs.Preferences; + +import com.oracle.javafx.scenebuilder.app.util.AppSettings; + +/** + * Utility Class to help preparing Preferences nodes for tests + */ +class PrefsHelper { + private PrefsHelper() { + /* not intended for instantiation */ + } + public static void removeAllNonReleaseNodes(Preferences prefs) { + String appVersion = AppSettings.getSceneBuilderVersion(); + String versionSpecificNode = PreferencesController.SB_RELEASE_NODE_PREFIX + appVersion; + try { + String[] childnodes = prefs.childrenNames(); + for (String childName : childnodes) { + if (!childName.equals(versionSpecificNode)) { + prefs.node(childName).removeNode(); + prefs.flush(); + } + } + } catch (BackingStoreException e) { + throw new RuntimeException(e); + } + } + + public static void removeAllChildNodes(Preferences prefs) { + try { + String[] childnodes = prefs.childrenNames(); + for (String childName : childnodes) { + prefs.node(childName).removeNode(); + prefs.flush(); + } + } catch (BackingStoreException e) { + throw new RuntimeException(e); + } + } +} diff --git a/app/src/test/java/com/oracle/javafx/scenebuilder/app/preferences/VersionedPreferencesFinderTest.java b/app/src/test/java/com/oracle/javafx/scenebuilder/app/preferences/VersionedPreferencesFinderTest.java new file mode 100644 index 000000000..d10713b30 --- /dev/null +++ b/app/src/test/java/com/oracle/javafx/scenebuilder/app/preferences/VersionedPreferencesFinderTest.java @@ -0,0 +1,106 @@ +/* + * Copyright (c) 2022, Gluon and/or its affiliates. + * All rights reserved. Use is subject to license terms. + * + * This file is available and licensed under the following license: + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions + * are met: + * + * - Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * - Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in + * the documentation and/or other materials provided with the distribution. + * - Neither the name of Oracle Corporation and Gluon nor the names of its + * contributors may be used to endorse or promote products derived + * from this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT + * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT + * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, + * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY + * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ +package com.oracle.javafx.scenebuilder.app.preferences; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; + +import java.util.List; +import java.util.Optional; +import java.util.prefs.Preferences; + +import org.junit.BeforeClass; +import org.junit.Test; + +public class VersionedPreferencesFinderTest { + + private static Preferences testNode; + + private static VersionedPreferencesFinder classUnderTest; + + @BeforeClass + public static void setup() { + testNode = Preferences.userRoot() + .node("/SBTEST") + .node("/com/oracle/javafx/scenebuilder/app/preferences"); + PrefsHelper.removeAllChildNodes(testNode); + classUnderTest = new VersionedPreferencesFinder("SB_", testNode); + } + + @Test + public void that_other_existing_version_preferences_are_detected() { + PrefsHelper.removeAllNonReleaseNodes(testNode); + testNode.node("SB_2.0").put("testkey", "testvalue"); + testNode.node("SB_8.5").put("testkey", "testvalue"); + testNode.node("SB_16.1.2").put("testkey", "testvalue"); + List detectedVersions = classUnderTest.getDetectedVersions(); + assertTrue(detectedVersions.size() >= 3); + } + + @Test + public void that_an_empty_list_provided_when_no_other_prefs_versions_exist() throws Exception { + PrefsHelper.removeAllChildNodes(testNode); + List detectedVersions = classUnderTest.getDetectedVersions(); + assertTrue(detectedVersions.isEmpty()); + } + + @Test + public void that_previous_versions_are_properly_detected() { + PrefsHelper.removeAllNonReleaseNodes(testNode); + testNode.node("SB_8.5").put("testkey", "testvalue"); + testNode.node("SB_2.0").put("testkey", "testvalue"); + List previousVersionPrefs = classUnderTest.getPreviousVersions(); + List previousVersions = previousVersionPrefs.stream() + .map(VersionedPreferences::version) + .toList(); + assertEquals(List.of(new AppVersion(8,5),new AppVersion(2,0)), previousVersions); + } + + @Test + public void that_most_recent_previous_version_is_detected() { + PrefsHelper.removeAllNonReleaseNodes(testNode); + testNode.node("SB_8.5").put("testkey", "testvalue"); + testNode.node("SB_2.0").put("testkey", "testvalue"); + Optional mostRecentPreviousVersion = classUnderTest.previousVersionPrefs(); + assertTrue(mostRecentPreviousVersion.isPresent()); + assertEquals(new AppVersion(8, 5), mostRecentPreviousVersion.get().version()); + assertEquals("SB_8.5", mostRecentPreviousVersion.get().node().name()); + } + + @Test + public void that_no_version_is_detected_in_case_that_only_newer_versions_exist() { + PrefsHelper.removeAllNonReleaseNodes(testNode); + testNode.node("SB_199.199.199").put("testkey", "testvalue"); + Optional mostRecentPreviousVersion = classUnderTest.previousVersionPrefs(); + assertTrue(mostRecentPreviousVersion.isEmpty()); + } +} diff --git a/docs/snippets/AppConfiguration.md b/docs/snippets/AppConfiguration.md new file mode 100644 index 000000000..7dd7beb62 --- /dev/null +++ b/docs/snippets/AppConfiguration.md @@ -0,0 +1,57 @@ +# SceneBuilder Configuration + +## Data Storage + +| Location | Responsible Classes | Description | +| :---------------------- | :--------------------------------------------------------- | :---------- | +| applicationDataFolder | `c.o.j.scenebuilder.app.AppPlatform` | Message box and user library folders are located here | +| messageBoxFolder | `c.o.j.scenebuilder.app.AppPlatform` | Contents of message box | +| userLibraryFolder | `c.o.j.scenebuilder.app.AppPlatform` | May contain JAR file with a JavaFX controls inside | +| logsFolder | `c.o.j.scenebuilder.app.AppPlatform` | Here the `scenebuilder-x.y.z.log` file is stored, usually inside the users profiles directory | +| Java Preferences | `c.o.j.scenebuilder.app.preferences.PreferencesController` | Standardized persistent storage of application settings | + + +### ApplicationDataFolder + +| Platform | until Version 17 | Version 18 and later | +| -------- | -------------------- |---------------------------- | +| Windows | `%APPDATA%\Scene Builder` | `%APPDATA%\Scene Builder-18.0.1` | +| Linux | tbd. | tbd. | +| MacOS | tbd. | tbd. | + +### UserLibraryFolder + +| Location | +| -------------------------- | +| `applicationDataFolder/Library` | + +### MessageBoxFolder + +| Location | +| ---------------------- | +| `applicationDataFolder/MB` | + +### LogsFolder + +| Version | Location | +| ------- | ---------------------------------- | +| <= 17 | `%USERPROFILE%\.scenebuilder\logs` | +| >= 18 | `%USERPROFILE%\.scenebuilder-18.0.0\logs` | + +### Preferences + +Node Structure until version 17: + +Root Node: `com.oracle.javafx.scenebuilder.app.preferences` + * SB_2.0 (`IMPORTED_GLUON_JARS`, `LAST_SENT_TRACKING_INFO_DATE`, `RECENT_ITEMS`, `REGISTRATION_EMAIL`, `REGISTRATION_HASH`, `REGISTRATION_OPT_IN`) + * ARTIFACTS + * DOCUMENTS (separate node for each document's settings) + * REPOSITIRIES + +Structure with versions >= 18.0.0: + +Root Node: `com.oracle.javafx.scenebuilder.app.preferences` + * SB_18.0.0 + * ARTIFACTS + * DOCUMENTS + * REPOSITIRIES diff --git a/kit/src/main/java/com/oracle/javafx/scenebuilder/kit/alert/SBAlert.java b/kit/src/main/java/com/oracle/javafx/scenebuilder/kit/alert/SBAlert.java index c38765b27..def6c49cc 100644 --- a/kit/src/main/java/com/oracle/javafx/scenebuilder/kit/alert/SBAlert.java +++ b/kit/src/main/java/com/oracle/javafx/scenebuilder/kit/alert/SBAlert.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2017 Gluon and/or its affiliates. + * Copyright (c) 2017, 2022 Gluon and/or its affiliates. * All rights reserved. Use is subject to license terms. * * This file is available and licensed under the following license: @@ -32,6 +32,7 @@ package com.oracle.javafx.scenebuilder.kit.alert; import javafx.scene.control.Alert; +import javafx.scene.control.ButtonType; import javafx.stage.Stage; /** @@ -39,6 +40,12 @@ */ public class SBAlert extends Alert { + public SBAlert(AlertType alertType, ButtonType... buttons) { + super(alertType, "", buttons); + getDialogPane().getStyleClass().add("SB-alert"); + getDialogPane().getStylesheets().add(SBAlert.class.getResource("Alert.css").toString()); + } + public SBAlert(AlertType alertType, Stage owner) { super(alertType); @@ -52,4 +59,6 @@ private void setIcons(Stage owner) { Stage alertStage = (Stage) getDialogPane().getScene().getWindow(); alertStage.getIcons().setAll(owner.getIcons()); } + + } diff --git a/kit/src/main/java/com/oracle/javafx/scenebuilder/kit/preferences/PreferencesControllerBase.java b/kit/src/main/java/com/oracle/javafx/scenebuilder/kit/preferences/PreferencesControllerBase.java index 9ad3e3a2f..db4450afb 100644 --- a/kit/src/main/java/com/oracle/javafx/scenebuilder/kit/preferences/PreferencesControllerBase.java +++ b/kit/src/main/java/com/oracle/javafx/scenebuilder/kit/preferences/PreferencesControllerBase.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2017, 2019, Gluon and/or its affiliates. + * Copyright (c) 2017, 2022, Gluon and/or its affiliates. * All rights reserved. Use is subject to license terms. * * This file is available and licensed under the following license: @@ -81,24 +81,31 @@ public abstract class PreferencesControllerBase { * * **************************************************************************/ + protected final Preferences applicationPreferences; protected final Preferences applicationRootPreferences; protected final PreferencesRecordGlobalBase recordGlobal; protected final Preferences documentsRootPreferences; - protected final Preferences artifactsRootPreferences; + protected final Preferences artifactsRootPreferences; protected final Preferences repositoriesRootPreferences; protected final MavenPreferences mavenPreferences; protected final RepositoryPreferences repositoryPreferences; - /*************************************************************************** * * * Constructors * * * **************************************************************************/ - public PreferencesControllerBase(String basePrefNodeName, PreferencesRecordGlobalBase recordGlobal) { - applicationRootPreferences = Preferences.userNodeForPackage(getClass()).node(basePrefNodeName); + public PreferencesControllerBase(Preferences rootNode, String basePrefNodeName, + PreferencesRecordGlobalBase recordGlobal) { + if (rootNode == null) { + applicationPreferences = Preferences.userNodeForPackage(getClass()); + applicationRootPreferences = applicationPreferences.node(basePrefNodeName); + } else { + applicationPreferences = rootNode; + applicationRootPreferences = applicationPreferences.node(basePrefNodeName); + } // Preferences global to the SB application this.recordGlobal = recordGlobal; @@ -120,26 +127,17 @@ public PreferencesControllerBase(String basePrefNodeName, PreferencesRecordGloba mavenPreferences = new MavenPreferences(); // create initial map of existing artifacts - try { - final String[] childrenNames = artifactsRootPreferences.childrenNames(); - for (String child : childrenNames) { - Preferences artifactPreferences = artifactsRootPreferences.node(child); - MavenArtifact mavenArtifact = new MavenArtifact(child); - mavenArtifact.setPath(artifactPreferences.get(PreferencesRecordArtifact.PATH, null)); - mavenArtifact.setDependencies(artifactPreferences.get(PreferencesRecordArtifact.DEPENDENCIES, null)); - mavenArtifact.setFilter(artifactPreferences.get(PreferencesRecordArtifact.FILTER, null)); - final PreferencesRecordArtifact recordArtifact = new PreferencesRecordArtifact( - artifactsRootPreferences, mavenArtifact); - mavenPreferences.addRecordArtifact(child, recordArtifact); - } - } catch (BackingStoreException ex) { - Logger.getLogger(PreferencesControllerBase.class.getName()).log(Level.SEVERE, null, ex); - } + initializeMavenPreferences(); // repositories repositoryPreferences = new RepositoryPreferences(); // create initial map of existing repositories + initializeRepositoryPreferences(); + + } + + public void initializeRepositoryPreferences() { try { final String[] childrenNames = repositoriesRootPreferences.childrenNames(); for (String child : childrenNames) { @@ -156,7 +154,24 @@ public PreferencesControllerBase(String basePrefNodeName, PreferencesRecordGloba } catch (BackingStoreException ex) { Logger.getLogger(PreferencesControllerBase.class.getName()).log(Level.SEVERE, null, ex); } + } + public void initializeMavenPreferences() { + try { + final String[] childrenNames = artifactsRootPreferences.childrenNames(); + for (String child : childrenNames) { + Preferences artifactPreferences = artifactsRootPreferences.node(child); + MavenArtifact mavenArtifact = new MavenArtifact(child); + mavenArtifact.setPath(artifactPreferences.get(PreferencesRecordArtifact.PATH, null)); + mavenArtifact.setDependencies(artifactPreferences.get(PreferencesRecordArtifact.DEPENDENCIES, null)); + mavenArtifact.setFilter(artifactPreferences.get(PreferencesRecordArtifact.FILTER, null)); + final PreferencesRecordArtifact recordArtifact = new PreferencesRecordArtifact( + artifactsRootPreferences, mavenArtifact); + mavenPreferences.addRecordArtifact(child, recordArtifact); + } + } catch (BackingStoreException ex) { + Logger.getLogger(PreferencesControllerBase.class.getName()).log(Level.SEVERE, null, ex); + } } /*************************************************************************** diff --git a/kit/src/main/java/com/oracle/javafx/scenebuilder/kit/preferences/PreferencesRecordArtifact.java b/kit/src/main/java/com/oracle/javafx/scenebuilder/kit/preferences/PreferencesRecordArtifact.java index 6fd84a87e..5c31af9f1 100644 --- a/kit/src/main/java/com/oracle/javafx/scenebuilder/kit/preferences/PreferencesRecordArtifact.java +++ b/kit/src/main/java/com/oracle/javafx/scenebuilder/kit/preferences/PreferencesRecordArtifact.java @@ -44,9 +44,9 @@ public class PreferencesRecordArtifact { private final Preferences artifactsRootPreferences; private Preferences artifactPreferences; - private final static String GROUPID = "groupID"; - private final static String ARTIFACTID = "artifactId"; - private final static String VERSION = "version"; + private final static String GROUPID = "groupID"; + private final static String ARTIFACTID = "artifactId"; + private final static String VERSION = "version"; public final static String DEPENDENCIES = "dependencies"; public final static String FILTER = "filter"; public final static String PATH = "path"; diff --git a/kit/src/main/java/com/oracle/javafx/scenebuilder/kit/preferences/PreferencesRecordRepository.java b/kit/src/main/java/com/oracle/javafx/scenebuilder/kit/preferences/PreferencesRecordRepository.java index 5c0dc241b..7ded2b299 100644 --- a/kit/src/main/java/com/oracle/javafx/scenebuilder/kit/preferences/PreferencesRecordRepository.java +++ b/kit/src/main/java/com/oracle/javafx/scenebuilder/kit/preferences/PreferencesRecordRepository.java @@ -44,11 +44,11 @@ public class PreferencesRecordRepository { private final Preferences repositoriesRootPreferences; private Preferences repositoryPreferences; - public final static String REPO_ID = "ID"; - public final static String REPO_TYPE = "type"; - public final static String REPO_URL = "URL"; + public final static String REPO_ID = "ID"; + public final static String REPO_TYPE = "type"; + public final static String REPO_URL = "URL"; public final static String REPO_USER = "User"; - public final static String REPO_PASS = "Password"; + public final static String REPO_PASS = "Password"; private final Repository repository; diff --git a/pom.xml b/pom.xml index 34d8e556f..e55f9430c 100644 --- a/pom.xml +++ b/pom.xml @@ -19,7 +19,7 @@ 17 1.1.0 5.0.0-jdk9 - 11 + 17 UTF-8 @@ -56,6 +56,7 @@ 3.8.1 false + ${maven.compiler.release}