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

Last change on this file since 18472 was 18472, checked in by taylor.smock, 3 years ago

Fix some coverity warnings

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