Ticket #9446: 9446_v2.patch

File 9446_v2.patch, 32.3 KB (added by gaben, 18 months ago)

minor documentation and import fixes, functionally the same

  • src/org/openstreetmap/josm/gui/io/SaveLayersDialog.java

     
    6464/**
    6565 * Dialog that pops up when the user closes a layer with modified data.
    6666 *
    67  * It asks for confirmation that all modification should be discarded and offers
     67 * It asks for confirmation that all modifications should be discarded and offer
    6868 * to save the layers to file or upload to server, depending on the type of layer.
    6969 */
    7070public class SaveLayersDialog extends JDialog implements TableModelListener {
     
    556556            for (final SaveLayerInfo layerInfo: toUpload) {
    557557                AbstractModifiableLayer layer = layerInfo.getLayer();
    558558                if (canceled) {
    559                     model.setUploadState(layer, UploadOrSaveState.CANCELED);
     559                    GuiHelper.runInEDTAndWait(() -> model.setUploadState(layer, UploadOrSaveState.CANCELED));
    560560                    continue;
    561561                }
    562                 monitor.subTask(tr("Preparing layer ''{0}'' for upload ...", layerInfo.getName()));
     562                GuiHelper.runInEDTAndWait(() -> monitor.subTask(tr("Preparing layer ''{0}'' for upload ...", layerInfo.getName())));
    563563
     564                // checkPreUploadConditions must not be run in the EDT to avoid deadlocks
    564565                if (!UploadAction.checkPreUploadConditions(layer)) {
    565                     model.setUploadState(layer, UploadOrSaveState.FAILED);
     566                    GuiHelper.runInEDTAndWait(() -> model.setUploadState(layer, UploadOrSaveState.FAILED));
    566567                    continue;
    567568                }
    568569
    569                 AbstractUploadDialog dialog = layer.getUploadDialog();
    570                 if (dialog != null) {
    571                     dialog.setVisible(true);
    572                     if (dialog.isCanceled()) {
    573                         model.setUploadState(layer, UploadOrSaveState.CANCELED);
    574                         continue;
    575                     }
    576                     dialog.rememberUserInput();
    577                 }
     570                GuiHelper.runInEDTAndWait(() -> uploadLayersUploadModelStateOnFinish(layer));
     571                currentTask = null;
     572            }
     573        }
    578574
    579                 currentTask = layer.createUploadTask(monitor);
    580                 if (currentTask == null) {
    581                     model.setUploadState(layer, UploadOrSaveState.FAILED);
    582                     continue;
    583                 }
    584                 Future<?> currentFuture = worker.submit(currentTask);
    585                 try {
    586                     // wait for the asynchronous task to complete
    587                     currentFuture.get();
    588                 } catch (CancellationException e) {
    589                     Logging.trace(e);
     575        /**
     576         * Update the {@link #model} state on upload finish
     577         * @param layer The layer that has been saved
     578         */
     579        private void uploadLayersUploadModelStateOnFinish(AbstractModifiableLayer layer) {
     580            AbstractUploadDialog dialog = layer.getUploadDialog();
     581            if (dialog != null) {
     582                dialog.setVisible(true);
     583                if (dialog.isCanceled()) {
    590584                    model.setUploadState(layer, UploadOrSaveState.CANCELED);
    591                 } catch (InterruptedException | ExecutionException e) {
    592                     Logging.error(e);
    593                     model.setUploadState(layer, UploadOrSaveState.FAILED);
    594                     ExceptionDialogUtil.explainException(e);
     585                    return;
    595586                }
    596                 if (currentTask.isCanceled()) {
    597                     model.setUploadState(layer, UploadOrSaveState.CANCELED);
    598                 } else if (currentTask.isFailed()) {
    599                     Logging.error(currentTask.getLastException());
    600                     ExceptionDialogUtil.explainException(currentTask.getLastException());
    601                     model.setUploadState(layer, UploadOrSaveState.FAILED);
    602                 } else {
    603                     model.setUploadState(layer, UploadOrSaveState.OK);
    604                 }
    605                 currentTask = null;
     587                dialog.rememberUserInput();
    606588            }
     589
     590            currentTask = layer.createUploadTask(monitor);
     591            if (currentTask == null) {
     592                model.setUploadState(layer, UploadOrSaveState.FAILED);
     593                return;
     594            }
     595            Future<?> currentFuture = worker.submit(currentTask);
     596            try {
     597                // wait for the asynchronous task to complete
     598                currentFuture.get();
     599            } catch (CancellationException e) {
     600                Logging.trace(e);
     601                model.setUploadState(layer, UploadOrSaveState.CANCELED);
     602            } catch (InterruptedException | ExecutionException e) {
     603                Logging.error(e);
     604                model.setUploadState(layer, UploadOrSaveState.FAILED);
     605                ExceptionDialogUtil.explainException(e);
     606            }
     607            if (currentTask.isCanceled()) {
     608                model.setUploadState(layer, UploadOrSaveState.CANCELED);
     609            } else if (currentTask.isFailed()) {
     610                Logging.error(currentTask.getLastException());
     611                ExceptionDialogUtil.explainException(currentTask.getLastException());
     612                model.setUploadState(layer, UploadOrSaveState.FAILED);
     613            } else {
     614                model.setUploadState(layer, UploadOrSaveState.OK);
     615            }
    607616        }
    608617
    609618        protected void saveLayers(List<SaveLayerInfo> toSave) {
     
    672681
    673682        @Override
    674683        public void run() {
     684            GuiHelper.runInEDTAndWait(() -> model.setMode(SaveLayersModel.Mode.UPLOADING_AND_SAVING));
     685            // We very specifically do not want to block the EDT or the worker thread when validating
     686            List<SaveLayerInfo> toUpload = model.getLayersToUpload();
     687            if (!toUpload.isEmpty()) {
     688                uploadLayers(toUpload);
     689            }
    675690            GuiHelper.runInEDTAndWait(() -> {
    676                 model.setMode(SaveLayersModel.Mode.UPLOADING_AND_SAVING);
    677                 List<SaveLayerInfo> toUpload = model.getLayersToUpload();
    678                 if (!toUpload.isEmpty()) {
    679                     uploadLayers(toUpload);
    680                 }
    681691                List<SaveLayerInfo> toSave = model.getLayersToSave();
    682692                if (!toSave.isEmpty()) {
    683693                    saveLayers(toSave);
  • src/org/openstreetmap/josm/actions/UploadAction.java

     
    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
     
    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;
     
    4247
    4348/**
    4449 * Action that opens a connection to the osm server and uploads all changes.
    45  *
     50 * <p>
    4651 * A dialog is displayed asking the user to specify a rectangle to grab.
    4752 * The url and account settings from the preferences are used.
     53 * <p>
     54 * If the upload fails, this action offers various options to resolve conflicts.
    4855 *
    49  * If the upload fails this action offers various options to resolve conflicts.
    50  *
    5156 * @author imi
    5257 */
    5358public class UploadAction extends AbstractUploadAction {
     
    5560     * The list of upload hooks. These hooks will be called one after the other
    5661     * when the user wants to upload data. Plugins can insert their own hooks here
    5762     * if they want to be able to veto an upload.
    58      *
     63     * <p>
    5964     * Be default, the standard upload dialog is the only element in the list.
    6065     * Plugins should normally insert their code before that, so that the upload
    6166     * dialog is the last thing shown before upload really starts; on occasion
     
    6772    private static final String IS_ASYNC_UPLOAD_ENABLED = "asynchronous.upload";
    6873
    6974    static {
    70         /**
    71          * Calls validator before upload.
    72          */
     75        /** Calls validator before upload. */
    7376        UPLOAD_HOOKS.add(new ValidateUploadHook());
    7477
    75         /**
    76          * Fixes database errors
    77          */
     78        /** Fixes database errors */
    7879        UPLOAD_HOOKS.add(new FixDataHook());
    7980
    80         /**
    81          * Checks server capabilities before upload.
    82          */
     81        /** Checks server capabilities before upload. */
    8382        UPLOAD_HOOKS.add(new ApiPreconditionCheckerHook());
    8483
    85         /**
    86          * Adjusts the upload order of new relations
    87          */
     84        /** Adjusts the upload order of new relations */
    8885        UPLOAD_HOOKS.add(new RelationUploadOrderHook());
    8986
    90         /**
    91          * Removes discardable tags like created_by on modified objects
    92          */
     87        /** Removes discardable tags like created_by on modified objects */
    9388        LATE_UPLOAD_HOOKS.add(new DiscardTagsHook());
    9489    }
    9590
     
    194189    }
    195190
    196191    /**
    197      * Check whether the preconditions are met to upload data in <code>apiData</code>.
    198      * Makes sure upload is allowed, primitives in <code>apiData</code> don't participate in conflicts and
     192     * Check whether the preconditions are met to upload data in {@code apiData}.
     193     * Makes sure upload is allowed, primitives in {@code apiData} don't participate in conflicts and
    199194     * runs the installed {@link UploadHook}s.
    200195     *
    201196     * @param layer the source layer of the data to be uploaded
     
    203198     * @return true, if the preconditions are met; false, otherwise
    204199     */
    205200    public static boolean checkPreUploadConditions(AbstractModifiableLayer layer, APIDataSet apiData) {
     201        try {
     202            return checkPreUploadConditionsAsync(layer, apiData, null).get();
     203        } catch (InterruptedException e) {
     204            Thread.currentThread().interrupt();
     205            throw new JosmRuntimeException(e);
     206        } catch (ExecutionException e) {
     207            throw new JosmRuntimeException(e);
     208        }
     209    }
     210
     211    /**
     212     * Check whether the preconditions are met to upload data in {@code apiData}.
     213     * Makes sure upload is allowed, primitives in {@code apiData} don't participate in conflicts and
     214     * runs the installed {@link UploadHook}s.
     215     *
     216     * @param layer the source layer of the data to be uploaded
     217     * @param apiData the data to be uploaded
     218     * @param onFinish {@code true} if the preconditions are met; {@code false}, otherwise
     219     * @return A future that completes when the checks are finished
     220     */
     221    public static Future<Boolean> checkPreUploadConditionsAsync(AbstractModifiableLayer layer, APIDataSet apiData, Consumer<Boolean> onFinish) {
     222        final CompletableFuture<Boolean> future = new CompletableFuture<>();
     223        if (onFinish != null) {
     224            future.thenAccept(onFinish);
     225        }
    206226        if (layer.isUploadDiscouraged() && warnUploadDiscouraged(layer)) {
    207             return false;
     227            future.complete(false);
    208228        }
    209229        if (layer instanceof OsmDataLayer) {
    210230            OsmDataLayer osmLayer = (OsmDataLayer) layer;
     
    211231            ConflictCollection conflicts = osmLayer.getConflicts();
    212232            if (apiData.participatesInConflict(conflicts)) {
    213233                alertUnresolvedConflicts(osmLayer);
    214                 return false;
     234                if (!future.isDone()) {
     235                    future.complete(false);
     236                }
    215237            }
    216238        }
    217239        // 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));
     240        if (!future.isDone()) {
     241            MainApplication.worker.execute(() -> {
     242                boolean hooks = true;
     243                if (apiData != null) {
     244                    hooks = UPLOAD_HOOKS.stream().allMatch(hook -> hook.checkUpload(apiData));
     245                }
     246                future.complete(hooks);
     247            });
    222248        }
    223 
    224         return true;
     249        return future;
    225250    }
    226251
    227252    /**
     
    235260            new Notification(tr("No changes to upload.")).show();
    236261            return;
    237262        }
    238         if (!checkPreUploadConditions(layer, apiData))
    239             return;
     263        checkPreUploadConditionsAsync(layer, apiData, passed -> GuiHelper.runInEDT(() -> {
     264            if (Boolean.TRUE.equals(passed)) {
     265                realUploadData(layer, apiData);
     266            } else {
     267                new Notification(tr("One of the upload verification processes failed")).show();
     268            }
     269        }));
     270    }
    240271
     272    /**
     273     * Uploads data to the OSM API.
     274     *
     275     * @param layer the source layer for the data to upload
     276     * @param apiData the primitives to be added, updated, or deleted
     277     */
     278    private static void realUploadData(final OsmDataLayer layer, final APIDataSet apiData) {
     279
    241280        ChangesetUpdater.check();
    242281
    243282        final UploadDialog dialog = UploadDialog.getUploadDialog();
     
    283322            Optional<AsynchronousUploadPrimitivesTask> asyncUploadTask = AsynchronousUploadPrimitivesTask.createAsynchronousUploadTask(
    284323                    uploadStrategySpecification, layer, apiData, cs);
    285324
    286             if (asyncUploadTask.isPresent()) {
    287                 MainApplication.worker.execute(asyncUploadTask.get());
    288             }
     325            asyncUploadTask.ifPresent(MainApplication.worker::execute);
    289326        } else {
    290327            MainApplication.worker.execute(new UploadPrimitivesTask(uploadStrategySpecification, layer, apiData, cs));
    291328        }
  • src/org/openstreetmap/josm/actions/upload/ValidateUploadHook.java

     
    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;
     
    3329/**
    3430 * The action that does the validate thing.
    3531 * <p>
    36  * This action iterates through all active tests and give them the data, so that
     32 * This action iterates through all active tests and gives them the data, so that
    3733 * each one can test it.
    3834 *
    3935 * @author frsantos
     
    4440    /**
    4541     * Validate the modified data before uploading
    4642     * @param apiDataSet contains primitives to be uploaded
    47      * @return true if upload should continue, else false
     43     * @return {@code true} if upload should continue, else false
    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         }
     61        }, null, OsmValidator.getEnabledTests(true), visited, null, true).run();
    8062
    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;
    96 
    97         return displayErrorScreen(errors);
     63        return returnCode.get();
    9864    }
    9965
    10066    /**
     
    10167     * Displays a screen where the actions that would be taken are displayed and
    10268     * give the user the possibility to cancel the upload.
    10369     * @param errors The errors displayed in the screen
    104      * @return <code>true</code>, if the upload should continue. <code>false</code>
    105      *          if the user requested cancel.
     70     * @return {@code true}, if the upload should continue.<br>
     71     *         {@code false}, if the user requested cancel.
    10672     */
    10773    private static boolean displayErrorScreen(List<TestError> errors) {
    10874        JPanel p = new JPanel(new GridBagLayout());
  • src/org/openstreetmap/josm/data/validation/ValidationTask.java

     
    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
     
    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}
     
    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;
    5578    }
    5679
     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);
     85    }
     86
    5787    @Override
    5888    protected void cancel() {
    5989        this.canceled = true;
     
    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
     
    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())) {
     
    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

     
    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;
     
    2526import java.util.logging.Level;
    2627import java.util.stream.Collectors;
    2728
     29import jakarta.json.JsonObject;
    2830import org.apache.commons.compress.utils.FileNameUtils;
    2931import org.openstreetmap.josm.actions.ExtensionFileFilter;
    3032import org.openstreetmap.josm.cli.CLIModule;
     
    176178            fileMonitor.beginTask(tr("Processing files..."), this.input.size());
    177179            for (String inputFile : this.input) {
    178180                if (inputFile.endsWith(".validator.mapcss")) {
    179                     this.processValidatorFile(inputFile);
     181                    processValidatorFile(inputFile);
    180182                } else if (inputFile.endsWith(".mapcss")) {
    181                     this.processMapcssFile(inputFile);
     183                    processMapcssFile(inputFile);
    182184                } else {
    183185                    this.processFile(inputFile);
    184186                }
     
    197199     * @param inputFile The mapcss file to validate
    198200     * @throws ParseException if the file does not match the mapcss syntax
    199201     */
    200     private void processMapcssFile(final String inputFile) throws ParseException {
     202    private static void processMapcssFile(final String inputFile) throws ParseException {
    201203        final MapCSSStyleSource styleSource = new MapCSSStyleSource(new File(inputFile).toURI().getPath(), inputFile, inputFile);
    202204        styleSource.loadStyleSource();
    203205        if (!styleSource.getErrors().isEmpty()) {
     
    214216     * @throws IOException if there is a problem reading the file
    215217     * @throws ParseException if the file does not match the validator mapcss syntax
    216218     */
    217     private void processValidatorFile(final String inputFile) throws ParseException, IOException {
     219    private static void processValidatorFile(final String inputFile) throws ParseException, IOException {
    218220        // Check asserts
    219221        Config.getPref().putBoolean("validator.check_assert_local_rules", true);
    220222        final MapCSSTagChecker mapCSSTagChecker = new MapCSSTagChecker();
     
    256258        OsmDataLayer dataLayer = null;
    257259        try {
    258260            Logging.info(task);
    259             OsmValidator.initializeTests();
    260261            dataLayer = MainApplication.getLayerManager().getLayersOfType(OsmDataLayer.class)
    261262                    .stream().filter(layer -> inputFileFile.equals(layer.getAssociatedFile()))
    262263                    .findFirst().orElseThrow(() -> new JosmRuntimeException(tr("Could not find a layer for {0}", inputFile)));
     
    269270                    }
    270271                }
    271272            }
    272             Collection<Test> tests = OsmValidator.getEnabledTests(false);
    273             if (Files.isRegularFile(Paths.get(outputFile)) && !Files.deleteIfExists(Paths.get(outputFile))) {
     273            Path path = Paths.get(outputFile);
     274            if (path.toFile().isFile() && !Files.deleteIfExists(path)) {
    274275                Logging.error("Could not delete {0}, attempting to append", outputFile);
    275276            }
    276277            GeoJSONMapRouletteWriter geoJSONMapRouletteWriter = new GeoJSONMapRouletteWriter(dataSet);
    277             try (OutputStream fileOutputStream = Files.newOutputStream(Paths.get(outputFile))) {
    278                 tests.parallelStream().forEach(test -> runTest(test, geoJSONMapRouletteWriter, fileOutputStream, dataSet));
     278            OsmValidator.initializeTests();
     279
     280            try (OutputStream fileOutputStream = Files.newOutputStream(path)) {
     281                // The first writeErrors catches anything that was written, for whatever reason. This is probably never
     282                // going to be called.
     283                ValidationTask validationTask = new ValidationTask(errors -> writeErrors(geoJSONMapRouletteWriter, fileOutputStream, errors),
     284                        progressMonitorFactory.get(), OsmValidator.getEnabledTests(false),
     285                        dataSet.allPrimitives(), Collections.emptyList(), false);
     286                // This avoids keeping errors in memory
     287                validationTask.setTestConsumer((t, test) -> {
     288                    writeErrors(geoJSONMapRouletteWriter, fileOutputStream, test.getErrors());
     289                    t.getErrors().removeIf(test.getErrors()::contains);
     290                });
     291                validationTask.run();
    279292            }
    280293        } finally {
    281294            if (dataLayer != null) {
     
    285298        }
    286299    }
    287300
     301    private void writeErrors(GeoJSONMapRouletteWriter geoJSONMapRouletteWriter, OutputStream fileOutputStream,
     302            Collection<TestError> errors) {
     303        for (TestError error : errors) {
     304            Optional<JsonObject> object = geoJSONMapRouletteWriter.write(error);
     305            if (object.isPresent()) {
     306                try {
     307                    writeToFile(fileOutputStream, object.get().toString().getBytes(StandardCharsets.UTF_8));
     308                } catch (IOException e) {
     309                    throw new JosmRuntimeException(e);
     310                }
     311            }
     312        }
     313    }
     314
    288315    /**
    289316     * Get the default output name
    290317     * @param inputString The input file
     
    302329    }
    303330
    304331    /**
    305      * Run a test
    306      * @param test The test to run
    307      * @param geoJSONMapRouletteWriter The object to use to create challenges
    308      * @param fileOutputStream The location to write data to
    309      * @param dataSet The dataset to check
    310      */
    311     private void runTest(final Test test, final GeoJSONMapRouletteWriter geoJSONMapRouletteWriter,
    312             final OutputStream fileOutputStream, DataSet dataSet) {
    313         test.startTest(progressMonitorFactory.get());
    314         test.visit(dataSet.allPrimitives());
    315         test.endTest();
    316         test.getErrors().stream().map(geoJSONMapRouletteWriter::write)
    317                 .filter(Optional::isPresent).map(Optional::get)
    318                 .map(jsonObject -> jsonObject.toString().getBytes(StandardCharsets.UTF_8)).forEach(bytes -> {
    319                     try {
    320                         writeToFile(fileOutputStream, bytes);
    321                     } catch (IOException e) {
    322                         throw new JosmRuntimeException(e);
    323                     }
    324                 });
    325         test.clear();
    326     }
    327 
    328     /**
    329332     * Write to a file. Synchronized to avoid writing to the same file in different threads.
    330333     *
    331334     * @param fileOutputStream The file output stream to read