diff --git a/core/resources/l10n/messages.properties b/core/resources/l10n/messages.properties index aa902f8f4..0e34aeb97 100644 --- a/core/resources/l10n/messages.properties +++ b/core/resources/l10n/messages.properties @@ -381,6 +381,10 @@ PreferencesExporter.chooser.title = Export the Preferences File PreferencesImporter.chooser.title = Import a Preferences File +PreferencesOptionPanel.title = Export settings +PreferencesOptionPanel.checkbox.userDirectories = Export user directories +PreferencesOptionPanel.checkbox.userDirectories.ttip = If checked, user directories (possible sensitive information) will be exported. + ! Welcome dialog welcome.dlg.title = Welcome to OpenRocket welcome.dlg.lbl.thankYou = Thank you for downloading OpenRocket diff --git a/core/src/net/sf/openrocket/startup/Preferences.java b/core/src/net/sf/openrocket/startup/Preferences.java index dd714c8bc..522f90c99 100644 --- a/core/src/net/sf/openrocket/startup/Preferences.java +++ b/core/src/net/sf/openrocket/startup/Preferences.java @@ -53,6 +53,7 @@ public abstract class Preferences implements ChangeSource { public static final String EXPORT_EVENT_COMMENTS = "ExportEventComments"; public static final String EXPORT_COMMENT_CHARACTER = "ExportCommentCharacter"; public static final String USER_LOCAL = "locale"; + public static final String DEFAULT_DIRECTORY = "defaultDirectory"; public static final String PLOT_SHOW_POINTS = "ShowPlotPoints"; @@ -80,6 +81,7 @@ public abstract class Preferences implements ChangeSource { public static final String MARKER_STYLE_ICON = "MarkerStyleIcon"; private static final String SHOW_MARKERS = "ShowMarkers"; private static final String SHOW_ROCKSIM_FORMAT_WARNING = "ShowRocksimFormatWarning"; + private static final String EXPORT_USER_DIRECTORIES = "ExportUserDirectories"; //Preferences related to 3D graphics public static final String OPENGL_ENABLED = "OpenGLIsEnabled"; @@ -231,7 +233,15 @@ public abstract class Preferences implements ChangeSource { public final void setShowRockSimFormatWarning(boolean check) { this.putBoolean(SHOW_ROCKSIM_FORMAT_WARNING, check); } - + + public final boolean getExportUserDirectories() { + return this.getBoolean(EXPORT_USER_DIRECTORIES, false); + } + + public final void setExportUserDirectories(boolean check) { + this.putBoolean(EXPORT_USER_DIRECTORIES, check); + } + public final double getDefaultMach() { return Application.getPreferences().getChoice(Preferences.DEFAULT_MACH_NUMBER, 0.9, 0.3); } diff --git a/swing/src/net/sf/openrocket/gui/components/PreferencesOptionPanel.java b/swing/src/net/sf/openrocket/gui/components/PreferencesOptionPanel.java new file mode 100644 index 000000000..34e33d9f2 --- /dev/null +++ b/swing/src/net/sf/openrocket/gui/components/PreferencesOptionPanel.java @@ -0,0 +1,40 @@ +package net.sf.openrocket.gui.components; + +import net.miginfocom.swing.MigLayout; +import net.sf.openrocket.l10n.Translator; +import net.sf.openrocket.startup.Application; +import net.sf.openrocket.startup.Preferences; + +import javax.swing.BorderFactory; +import javax.swing.JCheckBox; +import javax.swing.JPanel; +import java.awt.event.ItemEvent; +import java.awt.event.ItemListener; + +/** + * A panel that adds storage options for exporting preferences. + */ +public class PreferencesOptionPanel extends JPanel { + private static final Translator trans = Application.getTranslator(); + private static final Preferences prefs = Application.getPreferences(); + + public PreferencesOptionPanel() { + super(new MigLayout("fill, ins 0")); + + JPanel panel = new JPanel(new MigLayout("fill, ins 4lp")); + panel.setBorder(BorderFactory.createTitledBorder(trans.get("PreferencesOptionPanel.title"))); + + JCheckBox exportUserDirectories = new JCheckBox(trans.get("PreferencesOptionPanel.checkbox.userDirectories")); + exportUserDirectories.setToolTipText(trans.get("PreferencesOptionPanel.checkbox.userDirectories.ttip")); + exportUserDirectories.setSelected(prefs.getExportUserDirectories()); + exportUserDirectories.addItemListener(new ItemListener() { + @Override + public void itemStateChanged(ItemEvent e) { + prefs.setExportUserDirectories(e.getStateChange() == ItemEvent.SELECTED); + } + }); + panel.add(exportUserDirectories, "wrap 10lp"); + + this.add(panel, "growx, north"); + } +} diff --git a/swing/src/net/sf/openrocket/gui/util/PreferencesExporter.java b/swing/src/net/sf/openrocket/gui/util/PreferencesExporter.java index 02564fafb..d4d2d47ae 100644 --- a/swing/src/net/sf/openrocket/gui/util/PreferencesExporter.java +++ b/swing/src/net/sf/openrocket/gui/util/PreferencesExporter.java @@ -1,22 +1,56 @@ package net.sf.openrocket.gui.util; +import net.sf.openrocket.arch.SystemInfo; +import net.sf.openrocket.gui.components.PreferencesOptionPanel; +import net.sf.openrocket.gui.main.MRUDesignFile; import net.sf.openrocket.gui.widgets.SaveFileChooser; import net.sf.openrocket.l10n.Translator; import net.sf.openrocket.startup.Application; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import org.w3c.dom.Document; +import org.w3c.dom.Element; +import org.w3c.dom.NodeList; +import org.xml.sax.SAXException; import javax.swing.JFileChooser; +import javax.xml.parsers.DocumentBuilderFactory; +import javax.xml.parsers.ParserConfigurationException; +import javax.xml.transform.OutputKeys; +import javax.xml.transform.Transformer; +import javax.xml.transform.TransformerException; +import javax.xml.transform.TransformerFactory; +import javax.xml.transform.dom.DOMSource; +import javax.xml.transform.stream.StreamResult; +import java.awt.Dimension; import java.awt.Window; import java.io.File; +import java.io.FileInputStream; import java.io.FileOutputStream; import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; import java.util.prefs.BackingStoreException; import java.util.prefs.Preferences; public abstract class PreferencesExporter { private static final Translator trans = Application.getTranslator(); private static final Logger log = LoggerFactory.getLogger(PreferencesExporter.class); + private static final net.sf.openrocket.startup.Preferences prefs = Application.getPreferences(); + + private static final String[] userDirectoriesKeysToIgnore; // Preference keys to ignore when exporting user directories (= keys that export user directories) + private static final String[] prefixKeysToIgnore; // Preference keys to ignore when exporting user directories (= keys that start with these prefixes) + + static { + userDirectoriesKeysToIgnore = new String[] { + net.sf.openrocket.startup.Preferences.USER_THRUST_CURVES_KEY, + net.sf.openrocket.startup.Preferences.DEFAULT_DIRECTORY + }; + prefixKeysToIgnore = new String[] { + MRUDesignFile.MRU_FILE_LIST_PROPERTY + }; + } + public static boolean exportPreferences(Window parent, Preferences preferences) { JFileChooser chooser = new SaveFileChooser(); @@ -24,10 +58,15 @@ public abstract class PreferencesExporter { chooser.setAcceptAllFileFilterUsed(false); chooser.setFileFilter(FileHelper.XML_FILTER); chooser.setCurrentDirectory(((SwingPreferences) Application.getPreferences()).getDefaultDirectory()); + chooser.setAccessory(new PreferencesOptionPanel()); - - // TODO: add storageoptions to choose whether to export user directories or not (default to not) - + // TODO: update this dynamically instead of hard-coded values + // The macOS file chooser has an issue where it does not update its size when the accessory is added. + if (SystemInfo.getPlatform() == SystemInfo.Platform.MAC_OS) { + Dimension currentSize = chooser.getPreferredSize(); + Dimension newSize = new Dimension((int) (1.25 * currentSize.width), (int) (1.2 * currentSize.height)); + chooser.setPreferredSize(newSize); + } //// Ensures No Problems When Choosing File if (chooser.showSaveDialog(parent) != JFileChooser.APPROVE_OPTION) { @@ -35,7 +74,7 @@ public abstract class PreferencesExporter { return false; } - ((SwingPreferences) Application.getPreferences()).setDefaultDirectory(chooser.getCurrentDirectory()); + ((SwingPreferences) prefs).setDefaultDirectory(chooser.getCurrentDirectory()); File file = chooser.getSelectedFile(); if (file == null) { @@ -43,14 +82,20 @@ public abstract class PreferencesExporter { return false; } - file = FileHelper.forceExtension(file, "xml"); - if (!FileHelper.confirmWrite(file, parent)) { + final File newFile = FileHelper.forceExtension(file, "xml"); + if (!FileHelper.confirmWrite(newFile, parent)) { log.info("Cancelled export of preferences."); return false; } - try (FileOutputStream fos = new FileOutputStream(file)) { - preferences.exportSubtree(fos); + try (FileOutputStream fos = new FileOutputStream(newFile)) { + if (prefs.getExportUserDirectories()) { + // Export all preferences + preferences.exportSubtree(fos); + } else { + // Export all preferences except user directories + exportFilteredPreferences(preferences, fos); + } log.info("Preferences exported successfully."); } catch (IOException | BackingStoreException e) { log.warn("Error while importing preferences: " + e.getMessage()); @@ -58,4 +103,92 @@ public abstract class PreferencesExporter { return true; } + + private static void exportFilteredPreferences(Preferences preferences, FileOutputStream fos) throws BackingStoreException, IOException { + // Filter out user directories + Preferences root = Preferences.userRoot(); + String originalNodeName = ((SwingPreferences) prefs).getNodename(); + String nodeName = originalNodeName + "-temp"; + if (root.nodeExists(nodeName)) { + root.node(nodeName).removeNode(); + } + Preferences tempPrefs = root.node(nodeName); + + // Fill in all parameters to the temporary preferences, except for user directories + copyFilteredPreferences(preferences, tempPrefs, userDirectoriesKeysToIgnore, prefixKeysToIgnore); + + // Export the filtered preferences + try { + // Export the filtered preferences to a temporary file + Path tempFile = Files.createTempFile("ORprefs_" + System.currentTimeMillis(), ".xml"); + try (FileOutputStream tempFos = new FileOutputStream(tempFile.toFile())) { + tempPrefs.exportSubtree(tempFos); + } + + // Read and parse the temporary file + DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance(); + Document doc; + try (FileInputStream tempFis = new FileInputStream(tempFile.toFile())) { + doc = factory.newDocumentBuilder().parse(tempFis); + } + + // Find and rename the node + NodeList nodeList = doc.getElementsByTagName("node"); + for (int i = 0; i < nodeList.getLength(); i++) { + Element element = (Element) nodeList.item(i); + if (element.getAttribute("name").equals(nodeName)) { + element.setAttribute("name", ((SwingPreferences) prefs).getNodename()); + break; + } + } + + // Create a transformer to write the XML document to the FileOutputStream + Transformer transformer = TransformerFactory.newInstance().newTransformer(); + + // Set output properties to include the correct DOCTYPE declaration + transformer.setOutputProperty(OutputKeys.DOCTYPE_SYSTEM, "http://java.sun.com/dtd/preferences.dtd"); + transformer.setOutputProperty(OutputKeys.STANDALONE, "no"); + + // Write the XML document to the FileOutputStream + DOMSource source = new DOMSource(doc); + StreamResult result = new StreamResult(fos); + transformer.transform(source, result); + + // Clean up the temporary file + Files.deleteIfExists(tempFile); + } catch (ParserConfigurationException | TransformerException | SAXException e) { + e.printStackTrace(); + } finally { + root.node(nodeName).removeNode(); + } + } + + private static void copyFilteredPreferences(Preferences src, Preferences dest, String[] keysToIgnore, String[] prefixKeysToIgnore) throws BackingStoreException { + for (String key : src.keys()) { + boolean ignoreKey = false; + for (String keyToIgnore : keysToIgnore) { + if (key.equals(keyToIgnore)) { + ignoreKey = true; + break; + } + } + if (!ignoreKey) { + for (String prefixKeyToIgnore : prefixKeysToIgnore) { + if (key.startsWith(prefixKeyToIgnore)) { + ignoreKey = true; + break; + } + } + } + if (!ignoreKey) { + dest.put(key, src.get(key, null)); + } + } + + for (String childNodeName : src.childrenNames()) { + Preferences srcChild = src.node(childNodeName); + Preferences destChild = dest.node(childNodeName); + copyFilteredPreferences(srcChild, destChild, keysToIgnore, prefixKeysToIgnore); + } + } } diff --git a/swing/src/net/sf/openrocket/gui/util/PreferencesImporter.java b/swing/src/net/sf/openrocket/gui/util/PreferencesImporter.java index 7b3aba43f..32a4044db 100644 --- a/swing/src/net/sf/openrocket/gui/util/PreferencesImporter.java +++ b/swing/src/net/sf/openrocket/gui/util/PreferencesImporter.java @@ -34,9 +34,6 @@ public abstract class PreferencesImporter { File importFile = chooser.getSelectedFile(); try (FileInputStream fis = new FileInputStream(importFile)) { - - // TODO: don't import user directories? - Preferences.importPreferences(fis); log.info("Preferences imported successfully."); } catch (IOException | InvalidPreferencesFormatException e) { diff --git a/swing/src/net/sf/openrocket/gui/util/SwingPreferences.java b/swing/src/net/sf/openrocket/gui/util/SwingPreferences.java index 9ac18081f..686c5bce0 100644 --- a/swing/src/net/sf/openrocket/gui/util/SwingPreferences.java +++ b/swing/src/net/sf/openrocket/gui/util/SwingPreferences.java @@ -88,7 +88,9 @@ public class SwingPreferences extends net.sf.openrocket.startup.Preferences { PREFNODE = root.node(NODENAME); } - + public String getNodename() { + return NODENAME; + } ////////////////////// @@ -232,7 +234,7 @@ public class SwingPreferences extends net.sf.openrocket.startup.Preferences { } public File getDefaultDirectory() { - String file = getString("defaultDirectory", null); + String file = getString(net.sf.openrocket.startup.Preferences.DEFAULT_DIRECTORY, null); if (file == null) return null; return new File(file); @@ -245,7 +247,7 @@ public class SwingPreferences extends net.sf.openrocket.startup.Preferences { } else { d = dir.getAbsolutePath(); } - putString("defaultDirectory", d); + putString(net.sf.openrocket.startup.Preferences.DEFAULT_DIRECTORY, d); storeVersion(); }