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

Last change on this file since 18942 was 18942, checked in by GerdP, 14 months ago

fix #23419: Memory leak in SessionSaveAction

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