source: josm/trunk/src/org/openstreetmap/josm/actions/SessionSaveAction.java@ 19328

Last change on this file since 19328 was 19328, checked in by stoecker, 6 weeks ago

see #24104 - removed deprecated functions

File size: 24.3 KB
Line 
1// License: GPL. For details, see LICENSE file.
2package org.openstreetmap.josm.actions;
3
4import static org.openstreetmap.josm.gui.help.HelpUtil.ht;
5import static org.openstreetmap.josm.tools.I18n.marktr;
6import static org.openstreetmap.josm.tools.I18n.tr;
7import static org.openstreetmap.josm.tools.I18n.trn;
8
9import java.awt.Component;
10import java.awt.Dimension;
11import java.awt.GridBagConstraints;
12import java.awt.GridBagLayout;
13import java.awt.event.ActionEvent;
14import java.awt.event.KeyEvent;
15import java.io.File;
16import java.io.IOException;
17import java.lang.ref.WeakReference;
18import java.nio.file.Files;
19import java.util.ArrayList;
20import java.util.Arrays;
21import java.util.Collection;
22import java.util.EnumSet;
23import java.util.HashMap;
24import java.util.HashSet;
25import java.util.List;
26import java.util.Map;
27import java.util.Objects;
28import java.util.Set;
29import java.util.stream.Collectors;
30import java.util.stream.Stream;
31
32import javax.swing.BorderFactory;
33import javax.swing.JCheckBox;
34import javax.swing.JFileChooser;
35import javax.swing.JLabel;
36import javax.swing.JOptionPane;
37import javax.swing.JPanel;
38import javax.swing.JScrollPane;
39import javax.swing.JTabbedPane;
40import javax.swing.SwingConstants;
41import javax.swing.border.EtchedBorder;
42import javax.swing.filechooser.FileFilter;
43
44import org.openstreetmap.josm.data.PreferencesUtils;
45import org.openstreetmap.josm.data.preferences.BooleanProperty;
46import org.openstreetmap.josm.gui.ConditionalOptionPaneUtil;
47import org.openstreetmap.josm.gui.ExtendedDialog;
48import org.openstreetmap.josm.gui.HelpAwareOptionPane;
49import org.openstreetmap.josm.gui.MainApplication;
50import org.openstreetmap.josm.gui.MapFrame;
51import org.openstreetmap.josm.gui.MapFrameListener;
52import org.openstreetmap.josm.gui.Notification;
53import org.openstreetmap.josm.gui.layer.AbstractModifiableLayer;
54import org.openstreetmap.josm.gui.layer.Layer;
55import org.openstreetmap.josm.gui.layer.LayerManager.LayerAddEvent;
56import org.openstreetmap.josm.gui.layer.LayerManager.LayerChangeListener;
57import org.openstreetmap.josm.gui.layer.LayerManager.LayerOrderChangeEvent;
58import org.openstreetmap.josm.gui.layer.LayerManager.LayerRemoveEvent;
59import org.openstreetmap.josm.gui.util.GuiHelper;
60import org.openstreetmap.josm.gui.util.WindowGeometry;
61import org.openstreetmap.josm.gui.widgets.AbstractFileChooser;
62import org.openstreetmap.josm.io.session.PluginSessionExporter;
63import org.openstreetmap.josm.io.session.SessionLayerExporter;
64import org.openstreetmap.josm.io.session.SessionWriter;
65import org.openstreetmap.josm.plugins.PluginHandler;
66import org.openstreetmap.josm.spi.preferences.Config;
67import org.openstreetmap.josm.tools.GBC;
68import org.openstreetmap.josm.tools.JosmRuntimeException;
69import org.openstreetmap.josm.tools.Logging;
70import org.openstreetmap.josm.tools.MultiMap;
71import org.openstreetmap.josm.tools.Shortcut;
72import org.openstreetmap.josm.tools.UserCancelException;
73import org.openstreetmap.josm.tools.Utils;
74
75/**
76 * Saves a JOSM session
77 * @since 18466
78 */
79public class SessionSaveAction extends DiskAccessAction implements MapFrameListener, LayerChangeListener {
80
81 private transient List<Layer> layers;
82 private transient Map<Layer, SessionLayerExporter> exporters;
83 private transient MultiMap<Layer, Layer> dependencies;
84
85 private static final BooleanProperty SAVE_LOCAL_FILES_PROPERTY = new BooleanProperty("session.savelocal", true);
86 private static final BooleanProperty SAVE_PLUGIN_INFORMATION_PROPERTY = new BooleanProperty("session.saveplugins", false);
87 private static final String TOOLTIP_DEFAULT = tr("Save the current session.");
88 private static final String SAVE_SESSION = marktr("Save Session");
89
90 protected transient FileFilter joz = new ExtensionFileFilter("joz", "joz", tr("Session file (archive) (*.joz)"));
91 protected transient FileFilter jos = new ExtensionFileFilter("jos", "jos", tr("Session file (*.jos)"));
92
93 private File removeFileOnSuccess;
94
95 private static String tooltip = TOOLTIP_DEFAULT;
96 static File sessionFile;
97 static boolean isZipSessionFile;
98 private static boolean pluginData;
99 static List<WeakReference<Layer>> layersInSessionFile;
100
101 private static final SessionSaveAction instance = new SessionSaveAction();
102
103 /**
104 * Returns the instance
105 * @return the instance
106 */
107 public static SessionSaveAction getInstance() {
108 return instance;
109 }
110
111 /**
112 * Constructs a new {@code SessionSaveAction}.
113 */
114 public SessionSaveAction() {
115 this(true, false);
116 updateEnabledState();
117 }
118
119 /**
120 * Constructs a new {@code SessionSaveAction}.
121 * @param toolbar Register this action for the toolbar preferences?
122 * @param installAdapters False, if you don't want to install layer changed and selection changed adapters
123 */
124 protected SessionSaveAction(boolean toolbar, boolean installAdapters) {
125 this(tr(SAVE_SESSION), "session", TOOLTIP_DEFAULT,
126 Shortcut.registerShortcut("system:savesession", tr("File: {0}", tr("Save Session...")), KeyEvent.VK_S, Shortcut.ALT_CTRL),
127 toolbar, "save-session", installAdapters);
128 setHelpId(ht("/Action/SessionSave"));
129 }
130
131 protected SessionSaveAction(String name, String iconName, String tooltip,
132 Shortcut shortcut, boolean register, String toolbarId, boolean installAdapters) {
133
134 super(name, iconName, tooltip, shortcut, register, toolbarId, installAdapters);
135 addListeners();
136 }
137
138 @Override
139 public void actionPerformed(ActionEvent e) {
140 try {
141 saveSession(false, false);
142 } catch (UserCancelException exception) {
143 Logging.trace(exception);
144 }
145 }
146
147 @Override
148 public void destroy() {
149 removeListeners();
150 super.destroy();
151 }
152
153 /**
154 * Attempts to save the session.
155 * @param saveAs true shows the dialog
156 * @param forceSaveAll saves all layers
157 * @return if the session and all layers were successfully saved
158 * @throws UserCancelException when the user has cancelled the save process
159 */
160 public boolean saveSession(boolean saveAs, boolean forceSaveAll) throws UserCancelException {
161 try {
162 return saveSessionImpl(saveAs, forceSaveAll);
163 } finally {
164 cleanup();
165 }
166 }
167
168 private boolean saveSessionImpl(boolean saveAs, boolean forceSaveAll) throws UserCancelException {
169 if (!isEnabled()) {
170 return false;
171 }
172
173 removeFileOnSuccess = null;
174
175 SessionSaveAsDialog dlg = new SessionSaveAsDialog();
176 if (saveAs) {
177 dlg.showDialog();
178 if (dlg.getValue() != 1) {
179 throw new UserCancelException();
180 }
181 }
182
183 // TODO: resolve dependencies for layers excluded by the user
184 List<Layer> layersOut = layers.stream()
185 .filter(layer -> exporters.get(layer) != null && exporters.get(layer).shallExport())
186 .collect(Collectors.toList());
187
188 boolean zipRequired = layersOut.stream().map(exporters::get)
189 .anyMatch(ex -> ex != null && ex.requiresZip()) || pluginsWantToSave();
190
191 saveAs = !doGetFile(saveAs, zipRequired);
192
193 String fn = sessionFile.getName();
194
195 if (!saveAs && layersInSessionFile != null) {
196 List<String> missingLayers = layersInSessionFile.stream()
197 .map(WeakReference::get)
198 .filter(Objects::nonNull)
199 .filter(l -> !layersOut.contains(l))
200 .map(Layer::getName)
201 .collect(Collectors.toList());
202
203 if (!missingLayers.isEmpty() &&
204 !ConditionalOptionPaneUtil.showConfirmationDialog(
205 "savesession_layerremoved",
206 null,
207 new JLabel("<html>"
208 + trn("The following layer has been removed since the session was last saved:",
209 "The following layers have been removed since the session was last saved:", missingLayers.size())
210 + "<ul><li>"
211 + String.join("<li>", missingLayers)
212 + "</ul><br>"
213 + tr("You are about to overwrite the session file \"{0}\". Would you like to proceed?", fn)),
214 tr("Layers removed"), JOptionPane.OK_CANCEL_OPTION, JOptionPane.WARNING_MESSAGE,
215 JOptionPane.OK_OPTION)) {
216 throw new UserCancelException();
217 }
218 }
219 setCurrentLayers(layersOut);
220
221 updateSessionFile(fn);
222
223 Stream<Layer> layersToSaveStream = layersOut.stream()
224 .filter(layer -> layer.isSavable()
225 && layer instanceof AbstractModifiableLayer
226 && ((AbstractModifiableLayer) layer).requiresSaveToFile()
227 && exporters.get(layer) != null
228 && !exporters.get(layer).requiresZip());
229
230 boolean success = true;
231 if (forceSaveAll || Boolean.TRUE.equals(SAVE_LOCAL_FILES_PROPERTY.get())) {
232 // individual files must be saved before the session file as the location may change
233 if (layersToSaveStream
234 .map(layer -> SaveAction.getInstance().doSave(layer, true))
235 .collect(Collectors.toList()) // force evaluation of all elements
236 .contains(false)) {
237
238 new Notification(tr("Not all local files referenced by the session file could be saved."
239 + "<br>Make sure you save them before closing JOSM."))
240 .setIcon(JOptionPane.WARNING_MESSAGE)
241 .setDuration(Notification.TIME_LONG)
242 .show();
243 success = false;
244 }
245 } else if (layersToSaveStream.anyMatch(l -> true)) {
246 new Notification(tr("Not all local files referenced by the session file are saved yet."
247 + "<br>Make sure you save them before closing JOSM."))
248 .setIcon(JOptionPane.INFORMATION_MESSAGE)
249 .setDuration(Notification.TIME_LONG)
250 .show();
251 }
252
253 int active = -1;
254 Layer activeLayer = getLayerManager().getActiveLayer();
255 if (activeLayer != null) {
256 active = layersOut.indexOf(activeLayer);
257 }
258
259 final EnumSet<SessionWriter.SessionWriterFlags> flags = EnumSet.noneOf(SessionWriter.SessionWriterFlags.class);
260 if (pluginData || (Boolean.TRUE.equals(SAVE_PLUGIN_INFORMATION_PROPERTY.get()) && pluginsWantToSave())) {
261 flags.add(SessionWriter.SessionWriterFlags.SAVE_PLUGIN_INFORMATION);
262 }
263 if (isZipSessionFile) {
264 flags.add(SessionWriter.SessionWriterFlags.IS_ZIP);
265 }
266 SessionWriter sw = new SessionWriter(layersOut, active, exporters, dependencies, flags.toArray(new SessionWriter.SessionWriterFlags[0]));
267 try {
268 Notification savingNotification = showSavingNotification(sessionFile.getName());
269 sw.write(sessionFile);
270 SaveActionBase.addToFileOpenHistory(sessionFile);
271 if (removeFileOnSuccess != null) {
272 PreferencesUtils.removeFromList(Config.getPref(), "file-open.history", removeFileOnSuccess.getCanonicalPath());
273 Files.deleteIfExists(removeFileOnSuccess.toPath());
274 removeFileOnSuccess = null;
275 }
276 showSavedNotification(savingNotification, sessionFile.getName());
277 } catch (SecurityException ex) {
278 Logging.error(ex);
279 if (removeFileOnSuccess != null) {
280 final String path = removeFileOnSuccess.getPath();
281 GuiHelper.runInEDT(() -> {
282 Notification notification = new Notification(tr("Could not delete file: {0}<br>{1}", path, ex.getMessage()));
283 notification.setIcon(JOptionPane.WARNING_MESSAGE);
284 notification.show();
285 });
286 } else {
287 // We should never hit this, unless something changes in the try block.
288 throw new JosmRuntimeException(ex);
289 }
290 } catch (IOException ex) {
291 Logging.error(ex);
292 HelpAwareOptionPane.showMessageDialogInEDT(
293 MainApplication.getMainFrame(),
294 tr("<html>Could not save session file ''{0}''.<br>Error is:<br>{1}</html>",
295 sessionFile.getName(), Utils.escapeReservedCharactersHTML(ex.getMessage())),
296 tr("IO Error"),
297 JOptionPane.ERROR_MESSAGE,
298 null
299 );
300 success = false;
301 }
302 return success;
303 }
304
305 /**
306 * Sets the current session file. Asks the user if necessary
307 * @param saveAs always ask the user
308 * @param zipRequired zip
309 * @return if the user was asked
310 * @throws UserCancelException when the user has cancelled the save process
311 */
312 protected boolean doGetFile(boolean saveAs, boolean zipRequired) throws UserCancelException {
313 if (!saveAs && sessionFile != null) {
314
315 if (isZipSessionFile || !zipRequired)
316 return true;
317
318 Logging.info("Converting *.jos to *.joz because a new layer has been added that requires zip format");
319 String oldPath = sessionFile.getAbsolutePath();
320 int i = oldPath.lastIndexOf('.');
321 File jozFile = new File(i < 0 ? oldPath : oldPath.substring(0, i) + ".joz");
322 if (!jozFile.exists()) {
323 removeFileOnSuccess = sessionFile;
324 setCurrentSession(jozFile, true);
325 return true;
326 }
327 Logging.warn("Asking user to choose a new location for the *.joz file because it already exists");
328 }
329
330 doGetFileChooser(zipRequired);
331 return false;
332 }
333
334 protected void doGetFileChooser(boolean zipRequired) throws UserCancelException {
335 AbstractFileChooser fc;
336
337 if (zipRequired) {
338 fc = createAndOpenFileChooser(false, false, tr(SAVE_SESSION), joz, JFileChooser.FILES_ONLY, "lastDirectory");
339 } else {
340 fc = createAndOpenFileChooser(false, false, tr(SAVE_SESSION), Arrays.asList(jos, joz), jos,
341 JFileChooser.FILES_ONLY, "lastDirectory");
342 }
343
344 if (fc == null) {
345 throw new UserCancelException();
346 }
347
348 File f = fc.getSelectedFile();
349 FileFilter ff = fc.getFileFilter();
350 boolean zip;
351
352 if (zipRequired || joz.equals(ff)) {
353 zip = true;
354 } else if (jos.equals(ff)) {
355 zip = false;
356 } else {
357 zip = Utils.hasExtension(f.getName(), "joz");
358 }
359 setCurrentSession(f, zip);
360 }
361
362 /**
363 * The "Save Session" dialog
364 */
365 public class SessionSaveAsDialog extends ExtendedDialog {
366
367 /**
368 * Constructs a new {@code SessionSaveAsDialog}.
369 */
370 public SessionSaveAsDialog() {
371 super(MainApplication.getMainFrame(), tr(SAVE_SESSION), tr("Save As"), tr("Cancel"));
372 configureContextsensitiveHelp("Action/SessionSaveAs", true /* show help button */);
373 initialize();
374 setButtonIcons("save_as", "cancel");
375 setDefaultButton(1);
376 setRememberWindowGeometry(getClass().getName() + ".geometry",
377 WindowGeometry.centerInWindow(MainApplication.getMainFrame(), new Dimension(450, 450)));
378 setContent(build(), false);
379 }
380
381 /**
382 * Initializes some action fields.
383 */
384 private void initialize() {
385 layers = new ArrayList<>(getLayerManager().getLayers());
386 exporters = new HashMap<>();
387 dependencies = new MultiMap<>();
388
389 Set<Layer> noExporter = new HashSet<>();
390
391 for (Layer layer : layers) {
392 SessionLayerExporter exporter = null;
393 try {
394 exporter = SessionWriter.getSessionLayerExporter(layer);
395 } catch (IllegalArgumentException | JosmRuntimeException e) {
396 Logging.error(e);
397 }
398 if (exporter != null) {
399 exporters.put(layer, exporter);
400 Collection<Layer> deps = exporter.getDependencies();
401 if (deps != null) {
402 dependencies.putAll(layer, deps);
403 } else {
404 dependencies.putVoid(layer);
405 }
406 } else {
407 noExporter.add(layer);
408 exporters.put(layer, null);
409 }
410 }
411
412 int numNoExporter = 0;
413 while (numNoExporter != noExporter.size()) {
414 numNoExporter = noExporter.size();
415 updateExporters(noExporter);
416 }
417 }
418
419 private void updateExporters(Collection<Layer> noExporter) {
420 for (Layer layer : layers) {
421 if (noExporter.contains(layer)) continue;
422 for (Layer depLayer : dependencies.get(layer)) {
423 if (noExporter.contains(depLayer)) {
424 noExporter.add(layer);
425 exporters.put(layer, null);
426 return;
427 }
428 }
429 }
430 }
431
432 protected final Component build() {
433 JPanel op = new JPanel(new GridBagLayout());
434 JPanel ip = new JPanel(new GridBagLayout());
435 for (Layer layer : layers) {
436 Component exportPanel;
437 SessionLayerExporter exporter = exporters.get(layer);
438 if (exporter == null) {
439 if (!exporters.containsKey(layer)) throw new AssertionError();
440 exportPanel = getDisabledExportPanel(layer);
441 } else {
442 exportPanel = exporter.getExportPanel();
443 }
444 if (exportPanel == null) continue;
445 JPanel wrapper = new JPanel(new GridBagLayout());
446 wrapper.setBorder(BorderFactory.createEtchedBorder(EtchedBorder.RAISED));
447 wrapper.add(exportPanel, GBC.std().fill(GridBagConstraints.HORIZONTAL));
448 ip.add(wrapper, GBC.eol().fill(GridBagConstraints.HORIZONTAL).insets(2, 2, 4, 2));
449 }
450 ip.add(GBC.glue(0, 1), GBC.eol().fill(GridBagConstraints.VERTICAL));
451 JScrollPane sp = new JScrollPane(ip);
452 sp.setBorder(BorderFactory.createEmptyBorder());
453 JPanel p = new JPanel(new GridBagLayout());
454 p.add(sp, GBC.eol().fill());
455 final JTabbedPane tabs = new JTabbedPane();
456 tabs.addTab(tr("Layers"), p);
457 op.add(tabs, GBC.eol().fill());
458 JCheckBox chkSaveLocal = new JCheckBox(tr("Save all local files to disk"), SAVE_LOCAL_FILES_PROPERTY.get());
459 chkSaveLocal.addChangeListener(l -> SAVE_LOCAL_FILES_PROPERTY.put(chkSaveLocal.isSelected()));
460 op.add(chkSaveLocal, GBC.eol());
461 if (pluginsWantToSave()) {
462 JCheckBox chkSavePlugins = new JCheckBox(tr("Save plugin information to disk"), SAVE_PLUGIN_INFORMATION_PROPERTY.get());
463 chkSavePlugins.addChangeListener(l -> SAVE_PLUGIN_INFORMATION_PROPERTY.put(chkSavePlugins.isSelected()));
464 chkSavePlugins.setToolTipText(tr("Plugins may have additional information that can be saved"));
465 op.add(chkSavePlugins, GBC.eol());
466 }
467 return op;
468 }
469
470 protected final Component getDisabledExportPanel(Layer layer) {
471 JPanel p = new JPanel(new GridBagLayout());
472 JCheckBox include = new JCheckBox();
473 include.setEnabled(false);
474 JLabel lbl = new JLabel(layer.getName(), layer.getIcon(), SwingConstants.LEADING);
475 lbl.setToolTipText(tr("No exporter for this layer"));
476 lbl.setLabelFor(include);
477 lbl.setEnabled(false);
478 p.add(include, GBC.std());
479 p.add(lbl, GBC.std());
480 p.add(GBC.glue(1, 0), GBC.std().fill(GridBagConstraints.HORIZONTAL));
481 return p;
482 }
483 }
484
485 protected void addListeners() {
486 MainApplication.addMapFrameListener(this);
487 MainApplication.getLayerManager().addLayerChangeListener(this);
488 }
489
490 protected void removeListeners() {
491 MainApplication.removeMapFrameListener(this);
492 MainApplication.getLayerManager().removeLayerChangeListener(this);
493 }
494
495 @Override
496 protected void updateEnabledState() {
497 setEnabled(MainApplication.isDisplayingMapView());
498 }
499
500 @Override
501 public void mapFrameInitialized(MapFrame oldFrame, MapFrame newFrame) {
502 updateEnabledState();
503 }
504
505 @Override
506 public void layerAdded(LayerAddEvent e) {
507 // not used
508 }
509
510 @Override
511 public void layerRemoving(LayerRemoveEvent e) {
512 if (e.isLastLayer()) {
513 setCurrentSession(null, false);
514 }
515 }
516
517 @Override
518 public void layerOrderChanged(LayerOrderChangeEvent e) {
519 // not used
520 }
521
522 /**
523 * Update the session file
524 * @param fileName The filename to use. If there are no periods in the file, we update the extension.
525 * @throws UserCancelException If the user does not want to overwrite a previously existing file.
526 */
527 private static void updateSessionFile(String fileName) throws UserCancelException {
528 if (fileName.indexOf('.') == -1) {
529 sessionFile = new File(sessionFile.getPath() + (isZipSessionFile ? ".joz" : ".jos"));
530 if (!SaveActionBase.confirmOverwrite(sessionFile)) {
531 throw new UserCancelException();
532 }
533 }
534 }
535
536 /**
537 * Sets the current session file and the layers included in that file
538 * @param file file
539 * @param layers layers that are currently represented in the session file
540 * @param flags The flags for the current session
541 * @since 18833
542 */
543 public static void setCurrentSession(File file, List<Layer> layers, SessionWriter.SessionWriterFlags... flags) {
544 final EnumSet<SessionWriter.SessionWriterFlags> flagSet = EnumSet.noneOf(SessionWriter.SessionWriterFlags.class);
545 flagSet.addAll(Arrays.asList(flags));
546 setCurrentSession(file, layers, flagSet);
547 }
548
549 /**
550 * Sets the current session file and the layers included in that file
551 * @param file file
552 * @param layers layers that are currently represented in the session file
553 * @param flags The flags for the current session
554 * @since 18833
555 */
556 public static void setCurrentSession(File file, List<Layer> layers, Set<SessionWriter.SessionWriterFlags> flags) {
557 setCurrentLayers(layers);
558 setCurrentSession(file, flags.contains(SessionWriter.SessionWriterFlags.IS_ZIP));
559 pluginData = flags.contains(SessionWriter.SessionWriterFlags.SAVE_PLUGIN_INFORMATION);
560 }
561
562 /**
563 * Sets the current session file
564 * @param file file
565 * @param zip if it is a zip session file
566 */
567 public static void setCurrentSession(File file, boolean zip) {
568 sessionFile = file;
569 isZipSessionFile = zip;
570 if (file == null) {
571 tooltip = TOOLTIP_DEFAULT;
572 } else {
573 tooltip = tr("Save the current session file \"{0}\".", file.getName());
574 }
575 getInstance().setTooltip(tooltip);
576 }
577
578 /**
579 * Sets the layers that are currently represented in the session file
580 * @param layers layers
581 */
582 public static void setCurrentLayers(List<Layer> layers) {
583 layersInSessionFile = layers.stream()
584 .filter(AbstractModifiableLayer.class::isInstance)
585 .map(WeakReference::new)
586 .collect(Collectors.toList());
587 }
588
589 /**
590 * Returns the tooltip for the component
591 * @return the tooltip for the component
592 */
593 public static String getTooltip() {
594 return tooltip;
595 }
596
597 /**
598 * Check to see if any plugins want to save their state
599 * @return {@code true} if the plugin wants to save their state
600 */
601 private static boolean pluginsWantToSave() {
602 for (PluginSessionExporter exporter : PluginHandler.load(PluginSessionExporter.class)) {
603 if (exporter.requiresSaving()) {
604 return true;
605 }
606 }
607 return false;
608 }
609
610 protected void cleanup() {
611 layers = null;
612 exporters = null;
613 dependencies = null;
614 }
615
616}
Note: See TracBrowser for help on using the repository browser.