Ticket #9446: 9446.patch

File 9446.patch, 28.6 KB (added by taylor.smock, 2 years ago)

Add checkPreUploadConditionsAsync, use ValidationTask in more locations

  • src/org/openstreetmap/josm/actions/UploadAction.java

    diff --git a/src/org/openstreetmap/josm/actions/UploadAction.java b/src/org/openstreetmap/josm/actions/UploadAction.java
    index 4c80c15f92..6e7771b258 100644
    a b import java.util.LinkedList;  
    1111import java.util.List;
    1212import java.util.Map;
    1313import java.util.Optional;
     14import java.util.concurrent.CompletableFuture;
     15import java.util.concurrent.ExecutionException;
     16import java.util.concurrent.Future;
     17import java.util.function.Consumer;
    1418
    1519import javax.swing.JOptionPane;
    1620
    import org.openstreetmap.josm.io.ChangesetUpdater;  
    3640import org.openstreetmap.josm.io.UploadStrategySpecification;
    3741import org.openstreetmap.josm.spi.preferences.Config;
    3842import org.openstreetmap.josm.tools.ImageProvider;
     43import org.openstreetmap.josm.tools.JosmRuntimeException;
    3944import org.openstreetmap.josm.tools.Logging;
    4045import org.openstreetmap.josm.tools.Shortcut;
    4146import org.openstreetmap.josm.tools.Utils;
    public class UploadAction extends AbstractUploadAction {  
    203208     * @return true, if the preconditions are met; false, otherwise
    204209     */
    205210    public static boolean checkPreUploadConditions(AbstractModifiableLayer layer, APIDataSet apiData) {
     211        try {
     212            return checkPreUploadConditionsAsync(layer, apiData, null).get();
     213        } catch (InterruptedException e) {
     214            Thread.currentThread().interrupt();
     215            throw new JosmRuntimeException(e);
     216        } catch (ExecutionException e) {
     217            throw new JosmRuntimeException(e);
     218        }
     219    }
     220
     221    /**
     222     * Check whether the preconditions are met to upload data in <code>apiData</code>.
     223     * Makes sure upload is allowed, primitives in <code>apiData</code> don't participate in conflicts and
     224     * runs the installed {@link UploadHook}s.
     225     *
     226     * @param layer the source layer of the data to be uploaded
     227     * @param apiData the data to be uploaded
     228     * @param onFinish {@code true} if the preconditions are met; {@code false}, otherwise
     229     * @return A future that completes when the checks are finished
     230     */
     231    public static Future<Boolean> checkPreUploadConditionsAsync(AbstractModifiableLayer layer, APIDataSet apiData, Consumer<Boolean> onFinish) {
     232        final CompletableFuture<Boolean> future = new CompletableFuture<>();
     233        if (onFinish != null) {
     234            future.thenAccept(onFinish);
     235        }
    206236        if (layer.isUploadDiscouraged() && warnUploadDiscouraged(layer)) {
    207             return false;
     237            future.complete(false);
    208238        }
    209239        if (layer instanceof OsmDataLayer) {
    210240            OsmDataLayer osmLayer = (OsmDataLayer) layer;
    211241            ConflictCollection conflicts = osmLayer.getConflicts();
    212242            if (apiData.participatesInConflict(conflicts)) {
    213243                alertUnresolvedConflicts(osmLayer);
    214                 return false;
     244                if (!future.isDone()) {
     245                    future.complete(false);
     246                }
    215247            }
    216248        }
    217249        // Call all upload hooks in sequence.
    218         // FIXME: this should become an asynchronous task
    219         //
    220         if (apiData != null) {
    221             return UPLOAD_HOOKS.stream().allMatch(hook -> hook.checkUpload(apiData));
     250        if (!future.isDone()) {
     251            MainApplication.worker.execute(() -> {
     252                boolean hooks = true;
     253                if (apiData != null) {
     254                    hooks = UPLOAD_HOOKS.stream().allMatch(hook -> hook.checkUpload(apiData));
     255                }
     256                future.complete(hooks);
     257            });
    222258        }
    223 
    224         return true;
     259        return future;
    225260    }
    226261
    227262    /**
    public class UploadAction extends AbstractUploadAction {  
    235270            new Notification(tr("No changes to upload.")).show();
    236271            return;
    237272        }
    238         if (!checkPreUploadConditions(layer, apiData))
    239             return;
     273        checkPreUploadConditionsAsync(layer, apiData, passed -> GuiHelper.runInEDT(() -> {
     274            if (Boolean.TRUE.equals(passed)) {
     275                realUploadData(layer, apiData);
     276            } else {
     277                new Notification(tr("One of the upload verification processes failed")).show();
     278            }
     279        }));
     280    }
     281
     282    /**
     283     * Uploads data to the OSM API.
     284     *
     285     * @param layer the source layer for the data to upload
     286     * @param apiData the primitives to be added, updated, or deleted
     287     */
     288    private static void realUploadData(final OsmDataLayer layer, final APIDataSet apiData) {
    240289
    241290        ChangesetUpdater.check();
    242291
  • src/org/openstreetmap/josm/actions/upload/ValidateUploadHook.java

    diff --git a/src/org/openstreetmap/josm/actions/upload/ValidateUploadHook.java b/src/org/openstreetmap/josm/actions/upload/ValidateUploadHook.java
    index 06d704c75c..091b4db44b 100644
    a b import static org.openstreetmap.josm.tools.I18n.tr;  
    55
    66import java.awt.Dimension;
    77import java.awt.GridBagLayout;
    8 import java.util.ArrayList;
    98import java.util.Collection;
    109import java.util.List;
     10import java.util.concurrent.atomic.AtomicBoolean;
    1111
    1212import javax.swing.JPanel;
    1313import javax.swing.JScrollPane;
    1414
    1515import org.openstreetmap.josm.data.APIDataSet;
    1616import org.openstreetmap.josm.data.osm.OsmPrimitive;
    17 import org.openstreetmap.josm.data.preferences.sources.ValidatorPrefHelper;
    1817import org.openstreetmap.josm.data.validation.OsmValidator;
    19 import org.openstreetmap.josm.data.validation.Severity;
    20 import org.openstreetmap.josm.data.validation.Test;
    2118import org.openstreetmap.josm.data.validation.TestError;
     19import org.openstreetmap.josm.data.validation.ValidationTask;
    2220import org.openstreetmap.josm.data.validation.util.AggregatePrimitivesVisitor;
    2321import org.openstreetmap.josm.gui.ExtendedDialog;
    2422import org.openstreetmap.josm.gui.MainApplication;
    25 import org.openstreetmap.josm.gui.MapFrame;
    2623import org.openstreetmap.josm.gui.dialogs.validator.ValidatorTreePanel;
    27 import org.openstreetmap.josm.gui.layer.OsmDataLayer;
    2824import org.openstreetmap.josm.gui.layer.ValidatorLayer;
    2925import org.openstreetmap.josm.gui.util.GuiHelper;
    3026import org.openstreetmap.josm.gui.widgets.HtmlPanel;
    public class ValidateUploadHook implements UploadHook {  
    4844     */
    4945    @Override
    5046    public boolean checkUpload(APIDataSet apiDataSet) {
    51 
    52         OsmValidator.initializeTests();
    53         Collection<Test> tests = OsmValidator.getEnabledTests(true);
    54         if (tests.isEmpty())
    55             return true;
    56 
     47        AtomicBoolean returnCode = new AtomicBoolean();
    5748        AggregatePrimitivesVisitor v = new AggregatePrimitivesVisitor();
    5849        v.visit(apiDataSet.getPrimitivesToAdd());
    59         Collection<OsmPrimitive> selection = v.visit(apiDataSet.getPrimitivesToUpdate());
    60 
    61         List<TestError> errors = new ArrayList<>(30);
    62         for (Test test : tests) {
    63             test.setBeforeUpload(true);
    64             test.setPartialSelection(true);
    65             test.startTest(null);
    66             test.visit(selection);
    67             test.endTest();
    68             if (ValidatorPrefHelper.PREF_OTHER.get() && ValidatorPrefHelper.PREF_OTHER_UPLOAD.get()) {
    69                 errors.addAll(test.getErrors());
     50        Collection<OsmPrimitive> visited = v.visit(apiDataSet.getPrimitivesToUpdate());
     51        OsmValidator.initializeTests();
     52        new ValidationTask(errors -> {
     53            if (errors.stream().allMatch(TestError::isIgnored)) {
     54                returnCode.set(true);
    7055            } else {
    71                 for (TestError e : test.getErrors()) {
    72                     if (e.getSeverity() != Severity.OTHER) {
    73                         errors.add(e);
    74                     }
    75                 }
     56                // Unfortunately, the progress monitor is not "finished" until after `finish` is called, so we will
     57                // have a ProgressMonitor open behind the error screen. Fortunately, the error screen appears in front
     58                // of the progress monitor.
     59                GuiHelper.runInEDTAndWait(() -> returnCode.set(displayErrorScreen(errors)));
    7660            }
    77             test.clear();
    78             test.setBeforeUpload(false);
    79         }
    80 
    81         if (Boolean.TRUE.equals(ValidatorPrefHelper.PREF_USE_IGNORE.get())) {
    82             errors.forEach(TestError::updateIgnored);
    83         }
    84 
    85         OsmDataLayer editLayer = MainApplication.getLayerManager().getEditLayer();
    86         if (editLayer != null) {
    87             editLayer.validationErrors.clear();
    88             editLayer.validationErrors.addAll(errors);
    89         }
    90         MapFrame map = MainApplication.getMap();
    91         if (map != null) {
    92             map.validatorDialog.tree.setErrors(errors);
    93         }
    94         if (errors.stream().allMatch(TestError::isIgnored))
    95             return true;
     61        }, null, OsmValidator.getEnabledTests(true), visited, null, true).run();
    9662
    97         return displayErrorScreen(errors);
     63        return returnCode.get();
    9864    }
    9965
    10066    /**
  • src/org/openstreetmap/josm/data/validation/ValidationTask.java

    diff --git a/src/org/openstreetmap/josm/data/validation/ValidationTask.java b/src/org/openstreetmap/josm/data/validation/ValidationTask.java
    index b6e5d161eb..21eb1d3ca9 100644
    a b package org.openstreetmap.josm.data.validation;  
    33
    44import static org.openstreetmap.josm.tools.I18n.tr;
    55
     6import java.awt.GraphicsEnvironment;
    67import java.util.ArrayList;
    78import java.util.Collection;
    89import java.util.List;
     10import java.util.function.BiConsumer;
     11import java.util.function.Consumer;
    912
    1013import javax.swing.JOptionPane;
    1114
    import org.openstreetmap.josm.tools.Utils;  
    2528 * Asynchronous task for running a collection of tests against a collection of primitives
    2629 */
    2730public class ValidationTask extends PleaseWaitRunnable {
     31    private final Consumer<List<TestError>> onFinish;
    2832    private Collection<Test> tests;
    2933    private final Collection<OsmPrimitive> validatedPrimitives;
    3034    private final Collection<OsmPrimitive> formerValidatedPrimitives;
     35    private final boolean beforeUpload;
    3136    private boolean canceled;
    3237    private List<TestError> errors;
     38    private BiConsumer<ValidationTask, Test> testConsumer;
    3339
    3440    /**
    3541     * Constructs a new {@code ValidationTask}
    public class ValidationTask extends PleaseWaitRunnable {  
    4450        this(new PleaseWaitProgressMonitor(tr("Validating")), tests, validatedPrimitives, formerValidatedPrimitives);
    4551    }
    4652
    47     protected ValidationTask(ProgressMonitor progressMonitor,
    48                              Collection<Test> tests,
    49                              Collection<OsmPrimitive> validatedPrimitives,
    50                              Collection<OsmPrimitive> formerValidatedPrimitives) {
    51         super(tr("Validating"), progressMonitor, false /*don't ignore exceptions */);
     53    /**
     54     * Constructs a new {@code ValidationTask}
     55     *
     56     * @param onFinish                  called when the tests are finished
     57     * @param progressMonitor           the progress monitor to update with test progress
     58     * @param tests                     the tests to run
     59     * @param validatedPrimitives       the collection of primitives to validate.
     60     * @param formerValidatedPrimitives the last collection of primitives being validates. May be null.
     61     * @param beforeUpload              {@code true} if this is being run prior to upload
     62     * @since xxx
     63     */
     64    public ValidationTask(Consumer<List<TestError>> onFinish,
     65            ProgressMonitor progressMonitor,
     66            Collection<Test> tests,
     67            Collection<OsmPrimitive> validatedPrimitives,
     68            Collection<OsmPrimitive> formerValidatedPrimitives,
     69            boolean beforeUpload) {
     70        super(tr("Validating"),
     71                progressMonitor != null ? progressMonitor : new PleaseWaitProgressMonitor(tr("Validating")),
     72                false /*don't ignore exceptions */);
     73        this.onFinish = onFinish;
    5274        this.validatedPrimitives = validatedPrimitives;
    5375        this.formerValidatedPrimitives = formerValidatedPrimitives;
    5476        this.tests = tests;
     77        this.beforeUpload = beforeUpload;
     78    }
     79
     80    protected ValidationTask(ProgressMonitor progressMonitor,
     81            Collection<Test> tests,
     82            Collection<OsmPrimitive> validatedPrimitives,
     83            Collection<OsmPrimitive> formerValidatedPrimitives) {
     84        this(null, progressMonitor, tests, validatedPrimitives, formerValidatedPrimitives, false);
    5585    }
    5686
    5787    @Override
    public class ValidationTask extends PleaseWaitRunnable {  
    6393    protected void finish() {
    6494        if (canceled) return;
    6595
    66         // update GUI on Swing EDT
    67         GuiHelper.runInEDT(() -> {
    68             MapFrame map = MainApplication.getMap();
    69             map.validatorDialog.unfurlDialog();
    70             map.validatorDialog.tree.setErrors(errors);
    71             //FIXME: nicer way to find / invalidate the corresponding error layer
    72             MainApplication.getLayerManager().getLayersOfType(ValidatorLayer.class).forEach(ValidatorLayer::invalidate);
    73             if (!errors.isEmpty()) {
    74                 OsmValidator.initializeErrorLayer();
    75             }
    76         });
     96        if (!GraphicsEnvironment.isHeadless() && MainApplication.getMap() != null) {
     97            // update GUI on Swing EDT
     98            GuiHelper.runInEDT(() -> {
     99                MapFrame map = MainApplication.getMap();
     100                map.validatorDialog.unfurlDialog();
     101                map.validatorDialog.tree.setErrors(errors);
     102                //FIXME: nicer way to find / invalidate the corresponding error layer
     103                MainApplication.getLayerManager().getLayersOfType(ValidatorLayer.class).forEach(ValidatorLayer::invalidate);
     104                if (!errors.isEmpty()) {
     105                    OsmValidator.initializeErrorLayer();
     106                }
     107            });
     108        }
     109        if (this.onFinish != null) {
     110            this.onFinish.accept(this.errors);
     111        }
    77112    }
    78113
    79114    @Override
    public class ValidationTask extends PleaseWaitRunnable {  
    88123                return;
    89124            testCounter++;
    90125            getProgressMonitor().setCustomText(tr("Test {0}/{1}: Starting {2}", testCounter, tests.size(), test.getName()));
    91             test.setBeforeUpload(false);
     126            test.setBeforeUpload(this.beforeUpload);
    92127            test.setPartialSelection(formerValidatedPrimitives != null);
    93128            test.startTest(getProgressMonitor().createSubTaskMonitor(validatedPrimitives.size(), false));
    94129            test.visit(validatedPrimitives);
    95130            test.endTest();
    96131            errors.addAll(test.getErrors());
     132            if (this.testConsumer != null) {
     133                this.testConsumer.accept(this, test);
     134            }
    97135            test.clear();
     136            test.setBeforeUpload(false);
    98137        }
    99138        tests = null;
    100139        if (Boolean.TRUE.equals(ValidatorPrefHelper.PREF_USE_IGNORE.get())) {
    public class ValidationTask extends PleaseWaitRunnable {  
    124163    public List<TestError> getErrors() {
    125164        return errors;
    126165    }
     166
     167    /**
     168     * A test consumer to avoid filling up memory. A test consumer <i>may</i> remove tests it has consumed.
     169     * @param testConsumer The consumer which takes a {@link ValidationTask} ({@code this}) and the test that finished.
     170     * @since xxx
     171     */
     172    public void setTestConsumer(BiConsumer<ValidationTask, Test> testConsumer) {
     173        this.testConsumer = testConsumer;
     174    }
    127175}
  • src/org/openstreetmap/josm/data/validation/ValidatorCLI.java

    diff --git a/src/org/openstreetmap/josm/data/validation/ValidatorCLI.java b/src/org/openstreetmap/josm/data/validation/ValidatorCLI.java
    index cd4164c37f..0e7c787e7d 100644
    a b import java.io.InputStream;  
    1010import java.io.OutputStream;
    1111import java.nio.charset.StandardCharsets;
    1212import java.nio.file.Files;
     13import java.nio.file.Path;
    1314import java.nio.file.Paths;
    1415import java.util.ArrayList;
    1516import java.util.Arrays;
    import java.util.function.Supplier;  
    2526import java.util.logging.Level;
    2627import java.util.stream.Collectors;
    2728
     29import javax.json.JsonObject;
     30
    2831import org.apache.commons.compress.utils.FileNameUtils;
    2932import org.openstreetmap.josm.actions.ExtensionFileFilter;
    3033import org.openstreetmap.josm.cli.CLIModule;
    public class ValidatorCLI implements CLIModule {  
    174177            fileMonitor.beginTask(tr("Processing files..."), this.input.size());
    175178            for (String inputFile : this.input) {
    176179                if (inputFile.endsWith(".validator.mapcss")) {
    177                     this.processValidatorFile(inputFile);
     180                    processValidatorFile(inputFile);
    178181                } else if (inputFile.endsWith(".mapcss")) {
    179                     this.processMapcssFile(inputFile);
     182                    processMapcssFile(inputFile);
    180183                } else {
    181184                    this.processFile(inputFile);
    182185                }
    public class ValidatorCLI implements CLIModule {  
    195198     * @param inputFile The mapcss file to validate
    196199     * @throws ParseException if the file does not match the mapcss syntax
    197200     */
    198     private void processMapcssFile(final String inputFile) throws ParseException {
     201    private static void processMapcssFile(final String inputFile) throws ParseException {
    199202        final MapCSSStyleSource styleSource = new MapCSSStyleSource(new File(inputFile).toURI().getPath(), inputFile, inputFile);
    200203        styleSource.loadStyleSource();
    201204        if (!styleSource.getErrors().isEmpty()) {
    public class ValidatorCLI implements CLIModule {  
    212215     * @throws IOException if there is a problem reading the file
    213216     * @throws ParseException if the file does not match the validator mapcss syntax
    214217     */
    215     private void processValidatorFile(final String inputFile) throws ParseException, IOException {
     218    private static void processValidatorFile(final String inputFile) throws ParseException, IOException {
    216219        // Check asserts
    217220        Config.getPref().putBoolean("validator.check_assert_local_rules", true);
    218221        final MapCSSTagChecker mapCSSTagChecker = new MapCSSTagChecker();
    public class ValidatorCLI implements CLIModule {  
    254257        OsmDataLayer dataLayer = null;
    255258        try {
    256259            Logging.info(task);
    257             OsmValidator.initializeTests();
    258260            dataLayer = MainApplication.getLayerManager().getLayersOfType(OsmDataLayer.class)
    259261                    .stream().filter(layer -> inputFileFile.equals(layer.getAssociatedFile()))
    260262                    .findFirst().orElseThrow(() -> new JosmRuntimeException(tr("Could not find a layer for {0}", inputFile)));
    public class ValidatorCLI implements CLIModule {  
    267269                    }
    268270                }
    269271            }
    270             Collection<Test> tests = OsmValidator.getEnabledTests(false);
    271             if (Files.isRegularFile(Paths.get(outputFile)) && !Files.deleteIfExists(Paths.get(outputFile))) {
     272            Path path = Paths.get(outputFile);
     273            if (path.toFile().isFile() && !Files.deleteIfExists(path)) {
    272274                Logging.error("Could not delete {0}, attempting to append", outputFile);
    273275            }
    274276            GeoJSONMapRouletteWriter geoJSONMapRouletteWriter = new GeoJSONMapRouletteWriter(dataSet);
    275             try (OutputStream fileOutputStream = Files.newOutputStream(Paths.get(outputFile))) {
    276                 tests.parallelStream().forEach(test -> runTest(test, geoJSONMapRouletteWriter, fileOutputStream, dataSet));
     277            OsmValidator.initializeTests();
     278
     279            try (OutputStream fileOutputStream = Files.newOutputStream(path)) {
     280                // The first writeErrors catches anything that was written, for whatever reason. This is probably never
     281                // going to be called.
     282                ValidationTask validationTask = new ValidationTask(errors -> writeErrors(geoJSONMapRouletteWriter, fileOutputStream, errors),
     283                        progressMonitorFactory.get(), OsmValidator.getEnabledTests(false),
     284                        dataSet.allPrimitives(), Collections.emptyList(), false);
     285                // This avoids keeping errors in memory
     286                validationTask.setTestConsumer((t, test) -> {
     287                    writeErrors(geoJSONMapRouletteWriter, fileOutputStream, test.getErrors());
     288                    t.getErrors().removeIf(test.getErrors()::contains);
     289                });
     290                validationTask.run();
    277291            }
    278292        } finally {
    279293            if (dataLayer != null) {
    public class ValidatorCLI implements CLIModule {  
    283297        }
    284298    }
    285299
     300    private void writeErrors(GeoJSONMapRouletteWriter geoJSONMapRouletteWriter, OutputStream fileOutputStream,
     301            Collection<TestError> errors) {
     302        for (TestError error : errors) {
     303            Optional<JsonObject> object = geoJSONMapRouletteWriter.write(error);
     304            if (object.isPresent()) {
     305                try {
     306                    writeToFile(fileOutputStream, object.get().toString().getBytes(StandardCharsets.UTF_8));
     307                } catch (IOException e) {
     308                    throw new JosmRuntimeException(e);
     309                }
     310            }
     311        }
     312    }
     313
    286314    /**
    287315     * Get the default output name
    288316     * @param inputString The input file
    public class ValidatorCLI implements CLIModule {  
    299327        return FileNameUtils.getBaseName(FileNameUtils.getBaseName(inputString)) + ".geojson";
    300328    }
    301329
    302     /**
    303      * Run a test
    304      * @param test The test to run
    305      * @param geoJSONMapRouletteWriter The object to use to create challenges
    306      * @param fileOutputStream The location to write data to
    307      * @param dataSet The dataset to check
    308      */
    309     private void runTest(final Test test, final GeoJSONMapRouletteWriter geoJSONMapRouletteWriter,
    310             final OutputStream fileOutputStream, DataSet dataSet) {
    311         test.startTest(progressMonitorFactory.get());
    312         test.visit(dataSet.allPrimitives());
    313         test.endTest();
    314         test.getErrors().stream().map(geoJSONMapRouletteWriter::write)
    315                 .filter(Optional::isPresent).map(Optional::get)
    316                 .map(jsonObject -> jsonObject.toString().getBytes(StandardCharsets.UTF_8)).forEach(bytes -> {
    317                     try {
    318                         writeToFile(fileOutputStream, bytes);
    319                     } catch (IOException e) {
    320                         throw new JosmRuntimeException(e);
    321                     }
    322                 });
    323         test.clear();
    324     }
    325 
    326330    /**
    327331     * Write to a file. Synchronized to avoid writing to the same file in different threads.
    328332     *
  • src/org/openstreetmap/josm/gui/io/SaveLayersDialog.java

    diff --git a/src/org/openstreetmap/josm/gui/io/SaveLayersDialog.java b/src/org/openstreetmap/josm/gui/io/SaveLayersDialog.java
    index 9b4ffa7d9b..7718a4e272 100644
    a b public class SaveLayersDialog extends JDialog implements TableModelListener {  
    553553            for (final SaveLayerInfo layerInfo: toUpload) {
    554554                AbstractModifiableLayer layer = layerInfo.getLayer();
    555555                if (canceled) {
    556                     model.setUploadState(layer, UploadOrSaveState.CANCELED);
     556                    GuiHelper.runInEDTAndWait(() -> model.setUploadState(layer, UploadOrSaveState.CANCELED));
    557557                    continue;
    558558                }
    559                 monitor.subTask(tr("Preparing layer ''{0}'' for upload ...", layerInfo.getName()));
     559                GuiHelper.runInEDTAndWait(() -> monitor.subTask(tr("Preparing layer ''{0}'' for upload ...", layerInfo.getName())));
    560560
     561                // checkPreUploadConditions must not be run in the EDT to avoid deadlocks
    561562                if (!UploadAction.checkPreUploadConditions(layer)) {
    562                     model.setUploadState(layer, UploadOrSaveState.FAILED);
     563                    GuiHelper.runInEDTAndWait(() -> model.setUploadState(layer, UploadOrSaveState.FAILED));
    563564                    continue;
    564565                }
    565566
    566                 AbstractUploadDialog dialog = layer.getUploadDialog();
    567                 if (dialog != null) {
    568                     dialog.setVisible(true);
    569                     if (dialog.isCanceled()) {
    570                         model.setUploadState(layer, UploadOrSaveState.CANCELED);
    571                         continue;
    572                     }
    573                     dialog.rememberUserInput();
    574                 }
     567                GuiHelper.runInEDTAndWait(() -> uploadLayersUploadModelStateOnFinish(layer));
     568                currentTask = null;
     569            }
     570        }
    575571
    576                 currentTask = layer.createUploadTask(monitor);
    577                 if (currentTask == null) {
    578                     model.setUploadState(layer, UploadOrSaveState.FAILED);
    579                     continue;
    580                 }
    581                 Future<?> currentFuture = worker.submit(currentTask);
    582                 try {
    583                     // wait for the asynchronous task to complete
    584                     currentFuture.get();
    585                 } catch (CancellationException e) {
    586                     Logging.trace(e);
    587                     model.setUploadState(layer, UploadOrSaveState.CANCELED);
    588                 } catch (InterruptedException | ExecutionException e) {
    589                     Logging.error(e);
    590                     model.setUploadState(layer, UploadOrSaveState.FAILED);
    591                     ExceptionDialogUtil.explainException(e);
    592                 }
    593                 if (currentTask.isCanceled()) {
     572        /**
     573         * Update the {@link #model} state on upload finish
     574         * @param layer The layer that has been saved
     575         */
     576        private void uploadLayersUploadModelStateOnFinish(AbstractModifiableLayer layer) {
     577            AbstractUploadDialog dialog = layer.getUploadDialog();
     578            if (dialog != null) {
     579                dialog.setVisible(true);
     580                if (dialog.isCanceled()) {
    594581                    model.setUploadState(layer, UploadOrSaveState.CANCELED);
    595                 } else if (currentTask.isFailed()) {
    596                     Logging.error(currentTask.getLastException());
    597                     ExceptionDialogUtil.explainException(currentTask.getLastException());
    598                     model.setUploadState(layer, UploadOrSaveState.FAILED);
    599                 } else {
    600                     model.setUploadState(layer, UploadOrSaveState.OK);
     582                    return;
    601583                }
    602                 currentTask = null;
     584                dialog.rememberUserInput();
     585            }
     586
     587            currentTask = layer.createUploadTask(monitor);
     588            if (currentTask == null) {
     589                model.setUploadState(layer, UploadOrSaveState.FAILED);
     590                return;
     591            }
     592            Future<?> currentFuture = worker.submit(currentTask);
     593            try {
     594                // wait for the asynchronous task to complete
     595                currentFuture.get();
     596            } catch (CancellationException e) {
     597                Logging.trace(e);
     598                model.setUploadState(layer, UploadOrSaveState.CANCELED);
     599            } catch (InterruptedException | ExecutionException e) {
     600                Logging.error(e);
     601                model.setUploadState(layer, UploadOrSaveState.FAILED);
     602                ExceptionDialogUtil.explainException(e);
     603            }
     604            if (currentTask.isCanceled()) {
     605                model.setUploadState(layer, UploadOrSaveState.CANCELED);
     606            } else if (currentTask.isFailed()) {
     607                Logging.error(currentTask.getLastException());
     608                ExceptionDialogUtil.explainException(currentTask.getLastException());
     609                model.setUploadState(layer, UploadOrSaveState.FAILED);
     610            } else {
     611                model.setUploadState(layer, UploadOrSaveState.OK);
    603612            }
    604613        }
    605614
    public class SaveLayersDialog extends JDialog implements TableModelListener {  
    669678
    670679        @Override
    671680        public void run() {
     681            GuiHelper.runInEDTAndWait(() -> model.setMode(SaveLayersModel.Mode.UPLOADING_AND_SAVING));
     682            // We very specifically do not want to block the EDT or the worker thread when validating
     683            List<SaveLayerInfo> toUpload = model.getLayersToUpload();
     684            if (!toUpload.isEmpty()) {
     685                uploadLayers(toUpload);
     686            }
    672687            GuiHelper.runInEDTAndWait(() -> {
    673                 model.setMode(SaveLayersModel.Mode.UPLOADING_AND_SAVING);
    674                 List<SaveLayerInfo> toUpload = model.getLayersToUpload();
    675                 if (!toUpload.isEmpty()) {
    676                     uploadLayers(toUpload);
    677                 }
    678688                List<SaveLayerInfo> toSave = model.getLayersToSave();
    679689                if (!toSave.isEmpty()) {
    680690                    saveLayers(toSave);