001// License: GPL. For details, see LICENSE file.
002package org.openstreetmap.josm.gui.io;
003
004import static org.openstreetmap.josm.gui.help.HelpUtil.ht;
005import static org.openstreetmap.josm.tools.I18n.tr;
006import static org.openstreetmap.josm.tools.I18n.trn;
007
008import java.awt.Component;
009import java.awt.Dimension;
010import java.awt.FlowLayout;
011import java.awt.GridBagLayout;
012import java.awt.event.ActionEvent;
013import java.awt.event.WindowAdapter;
014import java.awt.event.WindowEvent;
015import java.beans.PropertyChangeEvent;
016import java.beans.PropertyChangeListener;
017import java.lang.Character.UnicodeBlock;
018import java.util.ArrayList;
019import java.util.Arrays;
020import java.util.Collection;
021import java.util.Collections;
022import java.util.HashMap;
023import java.util.Iterator;
024import java.util.List;
025import java.util.Locale;
026import java.util.Map;
027import java.util.Map.Entry;
028import java.util.Optional;
029import java.util.concurrent.TimeUnit;
030import java.util.stream.Collectors;
031
032import javax.swing.AbstractAction;
033import javax.swing.BorderFactory;
034import javax.swing.Icon;
035import javax.swing.JButton;
036import javax.swing.JOptionPane;
037import javax.swing.JPanel;
038import javax.swing.JTabbedPane;
039
040import org.openstreetmap.josm.data.APIDataSet;
041import org.openstreetmap.josm.data.Version;
042import org.openstreetmap.josm.data.osm.Changeset;
043import org.openstreetmap.josm.data.osm.DataSet;
044import org.openstreetmap.josm.data.osm.OsmPrimitive;
045import org.openstreetmap.josm.gui.ExtendedDialog;
046import org.openstreetmap.josm.gui.HelpAwareOptionPane;
047import org.openstreetmap.josm.gui.MainApplication;
048import org.openstreetmap.josm.gui.help.ContextSensitiveHelpAction;
049import org.openstreetmap.josm.gui.help.HelpUtil;
050import org.openstreetmap.josm.gui.util.GuiHelper;
051import org.openstreetmap.josm.gui.util.MultiLineFlowLayout;
052import org.openstreetmap.josm.gui.util.WindowGeometry;
053import org.openstreetmap.josm.io.OsmApi;
054import org.openstreetmap.josm.io.UploadStrategy;
055import org.openstreetmap.josm.io.UploadStrategySpecification;
056import org.openstreetmap.josm.spi.preferences.Config;
057import org.openstreetmap.josm.spi.preferences.PreferenceChangeEvent;
058import org.openstreetmap.josm.spi.preferences.PreferenceChangedListener;
059import org.openstreetmap.josm.spi.preferences.Setting;
060import org.openstreetmap.josm.tools.GBC;
061import org.openstreetmap.josm.tools.ImageOverlay;
062import org.openstreetmap.josm.tools.ImageProvider;
063import org.openstreetmap.josm.tools.ImageProvider.ImageSizes;
064import org.openstreetmap.josm.tools.InputMapUtils;
065import org.openstreetmap.josm.tools.Utils;
066
067/**
068 * This is a dialog for entering upload options like the parameters for
069 * the upload changeset and the strategy for opening/closing a changeset.
070 * @since 2025
071 */
072public class UploadDialog extends AbstractUploadDialog implements PropertyChangeListener, PreferenceChangedListener {
073    /** the unique instance of the upload dialog */
074    private static UploadDialog uploadDialog;
075
076    /** list of custom components that can be added by plugins at JOSM startup */
077    private static final Collection<Component> customComponents = new ArrayList<>();
078
079    /** the "created_by" changeset OSM key */
080    private static final String CREATED_BY = "created_by";
081
082    /** the panel with the objects to upload */
083    private UploadedObjectsSummaryPanel pnlUploadedObjects;
084    /** the panel to select the changeset used */
085    private ChangesetManagementPanel pnlChangesetManagement;
086
087    private BasicUploadSettingsPanel pnlBasicUploadSettings;
088
089    private UploadStrategySelectionPanel pnlUploadStrategySelectionPanel;
090
091    /** checkbox for selecting whether an atomic upload is to be used  */
092    private TagSettingsPanel pnlTagSettings;
093    /** the tabbed pane used below of the list of primitives  */
094    private JTabbedPane tpConfigPanels;
095    /** the upload button */
096    private JButton btnUpload;
097
098    /** the changeset comment model keeping the state of the changeset comment */
099    private final transient ChangesetCommentModel changesetCommentModel = new ChangesetCommentModel();
100    private final transient ChangesetCommentModel changesetSourceModel = new ChangesetCommentModel();
101    private final transient ChangesetReviewModel changesetReviewModel = new ChangesetReviewModel();
102
103    private transient DataSet dataSet;
104
105    /**
106     * Constructs a new {@code UploadDialog}.
107     */
108    public UploadDialog() {
109        super(GuiHelper.getFrameForComponent(MainApplication.getMainFrame()), ModalityType.DOCUMENT_MODAL);
110        build();
111        pack();
112    }
113
114    /**
115     * Replies the unique instance of the upload dialog
116     *
117     * @return the unique instance of the upload dialog
118     */
119    public static synchronized UploadDialog getUploadDialog() {
120        if (uploadDialog == null) {
121            uploadDialog = new UploadDialog();
122        }
123        return uploadDialog;
124    }
125
126    /**
127     * builds the content panel for the upload dialog
128     *
129     * @return the content panel
130     */
131    protected JPanel buildContentPanel() {
132        JPanel pnl = new JPanel(new GridBagLayout());
133        pnl.setBorder(BorderFactory.createEmptyBorder(5, 5, 5, 5));
134
135        // the panel with the list of uploaded objects
136        pnlUploadedObjects = new UploadedObjectsSummaryPanel();
137        pnl.add(pnlUploadedObjects, GBC.eol().fill(GBC.BOTH));
138
139        // Custom components
140        for (Component c : customComponents) {
141            pnl.add(c, GBC.eol().fill(GBC.HORIZONTAL));
142        }
143
144        // a tabbed pane with configuration panels in the lower half
145        tpConfigPanels = new CompactTabbedPane();
146
147        pnlBasicUploadSettings = new BasicUploadSettingsPanel(changesetCommentModel, changesetSourceModel, changesetReviewModel);
148        tpConfigPanels.add(pnlBasicUploadSettings);
149        tpConfigPanels.setTitleAt(0, tr("Settings"));
150        tpConfigPanels.setToolTipTextAt(0, tr("Decide how to upload the data and which changeset to use"));
151
152        pnlTagSettings = new TagSettingsPanel(changesetCommentModel, changesetSourceModel, changesetReviewModel);
153        tpConfigPanels.add(pnlTagSettings);
154        tpConfigPanels.setTitleAt(1, tr("Tags of new changeset"));
155        tpConfigPanels.setToolTipTextAt(1, tr("Apply tags to the changeset data is uploaded to"));
156
157        pnlChangesetManagement = new ChangesetManagementPanel(changesetCommentModel);
158        tpConfigPanels.add(pnlChangesetManagement);
159        tpConfigPanels.setTitleAt(2, tr("Changesets"));
160        tpConfigPanels.setToolTipTextAt(2, tr("Manage open changesets and select a changeset to upload to"));
161
162        pnlUploadStrategySelectionPanel = new UploadStrategySelectionPanel();
163        tpConfigPanels.add(pnlUploadStrategySelectionPanel);
164        tpConfigPanels.setTitleAt(3, tr("Advanced"));
165        tpConfigPanels.setToolTipTextAt(3, tr("Configure advanced settings"));
166
167        pnl.add(tpConfigPanels, GBC.eol().fill(GBC.HORIZONTAL));
168
169        pnl.add(buildActionPanel(), GBC.eol().fill(GBC.HORIZONTAL));
170        return pnl;
171    }
172
173    /**
174     * builds the panel with the OK and CANCEL buttons
175     *
176     * @return The panel with the OK and CANCEL buttons
177     */
178    protected JPanel buildActionPanel() {
179        JPanel pnl = new JPanel(new MultiLineFlowLayout(FlowLayout.CENTER));
180        pnl.setBorder(BorderFactory.createEmptyBorder(5, 5, 5, 5));
181
182        // -- upload button
183        btnUpload = new JButton(new UploadAction(this));
184        pnl.add(btnUpload);
185        btnUpload.setFocusable(true);
186        InputMapUtils.enableEnter(btnUpload);
187        InputMapUtils.addCtrlEnterAction(getRootPane(), btnUpload.getAction());
188
189        // -- cancel button
190        CancelAction cancelAction = new CancelAction(this);
191        pnl.add(new JButton(cancelAction));
192        InputMapUtils.addEscapeAction(getRootPane(), cancelAction);
193        pnl.add(new JButton(new ContextSensitiveHelpAction(ht("/Dialog/Upload"))));
194        HelpUtil.setHelpContext(getRootPane(), ht("/Dialog/Upload"));
195        return pnl;
196    }
197
198    /**
199     * builds the gui
200     */
201    protected void build() {
202        setTitle(tr("Upload to ''{0}''", OsmApi.getOsmApi().getBaseUrl()));
203        setContentPane(buildContentPanel());
204
205        addWindowListener(new WindowEventHandler());
206
207        // make sure the configuration panels listen to each other changes
208        //
209        pnlChangesetManagement.addPropertyChangeListener(this);
210        pnlChangesetManagement.addPropertyChangeListener(
211                pnlBasicUploadSettings.getUploadParameterSummaryPanel()
212        );
213        pnlChangesetManagement.addPropertyChangeListener(this);
214        pnlUploadedObjects.addPropertyChangeListener(
215                pnlBasicUploadSettings.getUploadParameterSummaryPanel()
216        );
217        pnlUploadedObjects.addPropertyChangeListener(pnlUploadStrategySelectionPanel);
218        pnlUploadStrategySelectionPanel.addPropertyChangeListener(
219                pnlBasicUploadSettings.getUploadParameterSummaryPanel()
220        );
221
222        // users can click on either of two links in the upload parameter
223        // summary handler. This installs the handler for these two events.
224        // We simply select the appropriate tab in the tabbed pane with the configuration dialogs.
225        //
226        pnlBasicUploadSettings.getUploadParameterSummaryPanel().setConfigurationParameterRequestListener(
227                new ConfigurationParameterRequestHandler() {
228                    @Override
229                    public void handleUploadStrategyConfigurationRequest() {
230                        tpConfigPanels.setSelectedIndex(3);
231                    }
232
233                    @Override
234                    public void handleChangesetConfigurationRequest() {
235                        tpConfigPanels.setSelectedIndex(2);
236                    }
237                }
238        );
239
240        pnlBasicUploadSettings.setUploadTagDownFocusTraversalHandlers(
241                new AbstractAction() {
242                    @Override
243                    public void actionPerformed(ActionEvent e) {
244                        btnUpload.requestFocusInWindow();
245                    }
246                }
247        );
248
249        setMinimumSize(new Dimension(600, 350));
250
251        Config.getPref().addPreferenceChangeListener(this);
252    }
253
254    /**
255     * Sets the collection of primitives to upload
256     *
257     * @param toUpload the dataset with the objects to upload. If null, assumes the empty
258     * set of objects to upload
259     *
260     */
261    public void setUploadedPrimitives(APIDataSet toUpload) {
262        if (toUpload == null) {
263            if (pnlUploadedObjects != null) {
264                List<OsmPrimitive> emptyList = Collections.emptyList();
265                pnlUploadedObjects.setUploadedPrimitives(emptyList, emptyList, emptyList);
266            }
267            return;
268        }
269        pnlUploadedObjects.setUploadedPrimitives(
270                toUpload.getPrimitivesToAdd(),
271                toUpload.getPrimitivesToUpdate(),
272                toUpload.getPrimitivesToDelete()
273        );
274    }
275
276    /**
277     * Sets the tags for this upload based on (later items overwrite earlier ones):
278     * <ul>
279     * <li>previous "source" and "comment" input</li>
280     * <li>the tags set in the dataset (see {@link DataSet#getChangeSetTags()})</li>
281     * <li>the tags from the selected open changeset</li>
282     * <li>the JOSM user agent (see {@link Version#getAgentString(boolean)})</li>
283     * </ul>
284     *
285     * @param dataSet to obtain the tags set in the dataset
286     */
287    public void setChangesetTags(DataSet dataSet) {
288        setChangesetTags(dataSet, false);
289    }
290
291    /**
292     * Sets the tags for this upload based on (later items overwrite earlier ones):
293     * <ul>
294     * <li>previous "source" and "comment" input</li>
295     * <li>the tags set in the dataset (see {@link DataSet#getChangeSetTags()})</li>
296     * <li>the tags from the selected open changeset</li>
297     * <li>the JOSM user agent (see {@link Version#getAgentString(boolean)})</li>
298     * </ul>
299     *
300     * @param dataSet to obtain the tags set in the dataset
301     * @param keepSourceComment if {@code true}, keep upload {@code source} and {@code comment} current values from models
302     */
303    private void setChangesetTags(DataSet dataSet, boolean keepSourceComment) {
304        final Map<String, String> tags = new HashMap<>();
305
306        // obtain from previous input
307        if (!keepSourceComment) {
308            tags.put("source", getLastChangesetSourceFromHistory());
309            tags.put("comment", getLastChangesetCommentFromHistory());
310        }
311
312        // obtain from dataset
313        if (dataSet != null) {
314            tags.putAll(dataSet.getChangeSetTags());
315        }
316        this.dataSet = dataSet;
317
318        // obtain from selected open changeset
319        if (pnlChangesetManagement.getSelectedChangeset() != null) {
320            tags.putAll(pnlChangesetManagement.getSelectedChangeset().getKeys());
321        }
322
323        // set/adapt created_by
324        final String agent = Version.getInstance().getAgentString(false);
325        final String createdBy = tags.get(CREATED_BY);
326        if (createdBy == null || createdBy.isEmpty()) {
327            tags.put(CREATED_BY, agent);
328        } else if (!createdBy.contains(agent)) {
329            tags.put(CREATED_BY, createdBy + ';' + agent);
330        }
331
332        // remove empty values
333        final Iterator<String> it = tags.keySet().iterator();
334        while (it.hasNext()) {
335            final String v = tags.get(it.next());
336            if (v == null || v.isEmpty()) {
337                it.remove();
338            }
339        }
340
341        // ignore source/comment to keep current values from models ?
342        if (keepSourceComment) {
343            tags.put("source", changesetSourceModel.getComment());
344            tags.put("comment", changesetCommentModel.getComment());
345        }
346
347        pnlTagSettings.initFromTags(tags);
348        pnlTagSettings.tableChanged(null);
349        pnlBasicUploadSettings.discardAllUndoableEdits();
350    }
351
352    @Override
353    public void rememberUserInput() {
354        pnlBasicUploadSettings.rememberUserInput();
355        pnlUploadStrategySelectionPanel.rememberUserInput();
356    }
357
358    /**
359     * Initializes the panel for user input
360     */
361    public void startUserInput() {
362        tpConfigPanels.setSelectedIndex(0);
363        pnlBasicUploadSettings.startUserInput();
364        pnlTagSettings.startUserInput();
365        pnlUploadStrategySelectionPanel.initFromPreferences();
366        UploadParameterSummaryPanel pnl = pnlBasicUploadSettings.getUploadParameterSummaryPanel();
367        pnl.setUploadStrategySpecification(pnlUploadStrategySelectionPanel.getUploadStrategySpecification());
368        pnl.setCloseChangesetAfterNextUpload(pnlChangesetManagement.isCloseChangesetAfterUpload());
369        pnl.setNumObjects(pnlUploadedObjects.getNumObjectsToUpload());
370    }
371
372    /**
373     * Replies the current changeset
374     *
375     * @return the current changeset
376     */
377    public Changeset getChangeset() {
378        Changeset cs = Optional.ofNullable(pnlChangesetManagement.getSelectedChangeset()).orElseGet(Changeset::new);
379        cs.setKeys(pnlTagSettings.getTags(false));
380        return cs;
381    }
382
383    /**
384     * Sets the changeset to be used in the next upload
385     *
386     * @param cs the changeset
387     */
388    public void setSelectedChangesetForNextUpload(Changeset cs) {
389        pnlChangesetManagement.setSelectedChangesetForNextUpload(cs);
390    }
391
392    @Override
393    public UploadStrategySpecification getUploadStrategySpecification() {
394        UploadStrategySpecification spec = pnlUploadStrategySelectionPanel.getUploadStrategySpecification();
395        spec.setCloseChangesetAfterUpload(pnlChangesetManagement.isCloseChangesetAfterUpload());
396        return spec;
397    }
398
399    @Override
400    public String getUploadComment() {
401        return changesetCommentModel.getComment();
402    }
403
404    @Override
405    public String getUploadSource() {
406        return changesetSourceModel.getComment();
407    }
408
409    @Override
410    public void setVisible(boolean visible) {
411        if (visible) {
412            new WindowGeometry(
413                    getClass().getName() + ".geometry",
414                    WindowGeometry.centerInWindow(
415                            MainApplication.getMainFrame(),
416                            new Dimension(400, 600)
417                    )
418            ).applySafe(this);
419            startUserInput();
420        } else if (isShowing()) { // Avoid IllegalComponentStateException like in #8775
421            new WindowGeometry(this).remember(getClass().getName() + ".geometry");
422        }
423        super.setVisible(visible);
424    }
425
426    /**
427     * Adds a custom component to this dialog.
428     * Custom components added at JOSM startup are displayed between the objects list and the properties tab pane.
429     * @param c The custom component to add. If {@code null}, this method does nothing.
430     * @return {@code true} if the collection of custom components changed as a result of the call
431     * @since 5842
432     */
433    public static boolean addCustomComponent(Component c) {
434        if (c != null) {
435            return customComponents.add(c);
436        }
437        return false;
438    }
439
440    static final class CompactTabbedPane extends JTabbedPane {
441        @Override
442        public Dimension getPreferredSize() {
443            // make sure the tabbed pane never grabs more space than necessary
444            return super.getMinimumSize();
445        }
446    }
447
448    /**
449     * Handles an upload.
450     */
451    static class UploadAction extends AbstractAction {
452
453        private final transient IUploadDialog dialog;
454
455        UploadAction(IUploadDialog dialog) {
456            this.dialog = dialog;
457            putValue(NAME, tr("Upload Changes"));
458            new ImageProvider("upload").getResource().attachImageIcon(this, true);
459            putValue(SHORT_DESCRIPTION, tr("Upload the changed primitives"));
460        }
461
462        /**
463         * Displays a warning message indicating that the upload comment is empty/short.
464         * @return true if the user wants to revisit, false if they want to continue
465         */
466        protected boolean warnUploadComment() {
467            return warnUploadTag(
468                    tr("Please revise upload comment"),
469                    tr("Your upload comment is <i>empty</i>, or <i>very short</i>.<br /><br />" +
470                            "This is technically allowed, but please consider that many users who are<br />" +
471                            "watching changes in their area depend on meaningful changeset comments<br />" +
472                            "to understand what is going on!<br /><br />" +
473                            "If you spend a minute now to explain your change, you will make life<br />" +
474                            "easier for many other mappers."),
475                    "upload_comment_is_empty_or_very_short"
476            );
477        }
478
479        /**
480         * Displays a warning message indicating that no changeset source is given.
481         * @return true if the user wants to revisit, false if they want to continue
482         */
483        protected boolean warnUploadSource() {
484            return warnUploadTag(
485                    tr("Please specify a changeset source"),
486                    tr("You did not specify a source for your changes.<br />" +
487                            "It is technically allowed, but this information helps<br />" +
488                            "other users to understand the origins of the data.<br /><br />" +
489                            "If you spend a minute now to explain your change, you will make life<br />" +
490                            "easier for many other mappers."),
491                    "upload_source_is_empty"
492            );
493        }
494
495        /**
496         * Displays a warning message indicating that the upload comment is rejected.
497         * @param details details explaining why
498         * @return {@code true}
499         */
500        protected boolean warnRejectedUploadComment(String details) {
501            return warnRejectedUploadTag(
502                    tr("Please revise upload comment"),
503                    tr("Your upload comment is <i>rejected</i>.") + "<br />" + details
504            );
505        }
506
507        /**
508         * Displays a warning message indicating that the changeset source is rejected.
509         * @param details details explaining why
510         * @return {@code true}
511         */
512        protected boolean warnRejectedUploadSource(String details) {
513            return warnRejectedUploadTag(
514                    tr("Please revise changeset source"),
515                    tr("Your changeset source is <i>rejected</i>.") + "<br />" + details
516            );
517        }
518
519        /**
520         * Warn about an upload tag with the possibility of resuming the upload.
521         * @param title dialog title
522         * @param message dialog message
523         * @param togglePref preference entry to offer the user a "Do not show again" checkbox for the dialog
524         * @return {@code true} if the user wants to revise the upload tag
525         */
526        protected boolean warnUploadTag(final String title, final String message, final String togglePref) {
527            return warnUploadTag(title, message, togglePref, true);
528        }
529
530        /**
531         * Warn about an upload tag without the possibility of resuming the upload.
532         * @param title dialog title
533         * @param message dialog message
534         * @return {@code true}
535         */
536        protected boolean warnRejectedUploadTag(final String title, final String message) {
537            return warnUploadTag(title, message, null, false);
538        }
539
540        private boolean warnUploadTag(final String title, final String message, final String togglePref, boolean allowContinue) {
541            List<String> buttonTexts = new ArrayList<>(Arrays.asList(tr("Revise"), tr("Cancel")));
542            List<Icon> buttonIcons = new ArrayList<>(Arrays.asList(
543                    new ImageProvider("ok").setMaxSize(ImageSizes.LARGEICON).get(),
544                    new ImageProvider("cancel").setMaxSize(ImageSizes.LARGEICON).get()));
545            List<String> tooltips = new ArrayList<>(Arrays.asList(
546                    tr("Return to the previous dialog to enter a more descriptive comment"),
547                    tr("Cancel and return to the previous dialog")));
548            if (allowContinue) {
549                buttonTexts.add(tr("Continue as is"));
550                buttonIcons.add(new ImageProvider("upload").setMaxSize(ImageSizes.LARGEICON).addOverlay(
551                        new ImageOverlay(new ImageProvider("warning-small"), 0.5, 0.5, 1.0, 1.0)).get());
552                tooltips.add(tr("Ignore this hint and upload anyway"));
553            }
554
555            ExtendedDialog dlg = new ExtendedDialog((Component) dialog, title, buttonTexts.toArray(new String[] {})) {
556                @Override
557                public void setupDialog() {
558                    super.setupDialog();
559                    InputMapUtils.addCtrlEnterAction(getRootPane(), buttons.get(buttons.size() - 1).getAction());
560                }
561            };
562            dlg.setContent("<html>" + message + "</html>");
563            dlg.setButtonIcons(buttonIcons.toArray(new Icon[] {}));
564            dlg.setToolTipTexts(tooltips.toArray(new String[] {}));
565            dlg.setIcon(JOptionPane.WARNING_MESSAGE);
566            if (allowContinue) {
567                dlg.toggleEnable(togglePref);
568            }
569            dlg.setCancelButton(1, 2);
570            return dlg.showDialog().getValue() != 3;
571        }
572
573        protected void warnIllegalChunkSize() {
574            HelpAwareOptionPane.showOptionDialog(
575                    (Component) dialog,
576                    tr("Please enter a valid chunk size first"),
577                    tr("Illegal chunk size"),
578                    JOptionPane.ERROR_MESSAGE,
579                    ht("/Dialog/Upload#IllegalChunkSize")
580            );
581        }
582
583        static boolean isUploadCommentTooShort(String comment) {
584            String s = Utils.strip(comment);
585            boolean result = true;
586            if (!s.isEmpty()) {
587                UnicodeBlock block = Character.UnicodeBlock.of(s.charAt(0));
588                if (block != null && block.toString().contains("CJK")) {
589                    result = s.length() < 4;
590                } else {
591                    result = s.length() < 10;
592                }
593            }
594            return result;
595        }
596
597        private static String lc(String s) {
598            return s.toLowerCase(Locale.ENGLISH);
599        }
600
601        static String validateUploadTag(String uploadValue, String preferencePrefix, List<String> defMandatory, List<String> defForbidden) {
602            String uploadValueLc = lc(uploadValue);
603            // Check mandatory terms
604            List<String> missingTerms = Config.getPref().getList(preferencePrefix+".mandatory-terms", defMandatory)
605                .stream().map(UploadAction::lc).filter(x -> !uploadValueLc.contains(x)).collect(Collectors.toList());
606            if (!missingTerms.isEmpty()) {
607                return tr("The following required terms are missing: {0}", missingTerms);
608            }
609            // Check forbidden terms
610            List<String> forbiddenTerms = Config.getPref().getList(preferencePrefix+".forbidden-terms", defForbidden)
611                    .stream().map(UploadAction::lc).filter(uploadValueLc::contains).collect(Collectors.toList());
612            if (!forbiddenTerms.isEmpty()) {
613                return tr("The following forbidden terms have been found: {0}", forbiddenTerms);
614            }
615            return null;
616        }
617
618        @Override
619        public void actionPerformed(ActionEvent e) {
620            // force update of model in case dialog is closed before focus lost event, see #17452
621            dialog.forceUpdateActiveField();
622
623            final List<String> def = Collections.emptyList();
624            final String uploadComment = dialog.getUploadComment();
625            final String uploadCommentRejection = validateUploadTag(
626                    uploadComment, "upload.comment", def, def);
627            if ((isUploadCommentTooShort(uploadComment) && warnUploadComment()) ||
628                (uploadCommentRejection != null && warnRejectedUploadComment(uploadCommentRejection))) {
629                // abort for missing or rejected comment
630                dialog.handleMissingComment();
631                return;
632            }
633            final String uploadSource = dialog.getUploadSource();
634            final String uploadSourceRejection = validateUploadTag(
635                    uploadSource, "upload.source", def, Collections.singletonList("google"));
636            if ((Utils.isStripEmpty(uploadSource) && warnUploadSource()) ||
637                    (uploadSourceRejection != null && warnRejectedUploadSource(uploadSourceRejection))) {
638                // abort for missing or rejected changeset source
639                dialog.handleMissingSource();
640                return;
641            }
642
643            /* test for empty tags in the changeset metadata and proceed only after user's confirmation.
644             * though, accept if key and value are empty (cf. xor). */
645            List<String> emptyChangesetTags = new ArrayList<>();
646            for (final Entry<String, String> i : dialog.getTags(true).entrySet()) {
647                final boolean isKeyEmpty = Utils.isStripEmpty(i.getKey());
648                final boolean isValueEmpty = Utils.isStripEmpty(i.getValue());
649                final boolean ignoreKey = "comment".equals(i.getKey()) || "source".equals(i.getKey());
650                if ((isKeyEmpty ^ isValueEmpty) && !ignoreKey) {
651                    emptyChangesetTags.add(tr("{0}={1}", i.getKey(), i.getValue()));
652                }
653            }
654            if (!emptyChangesetTags.isEmpty() && JOptionPane.OK_OPTION != JOptionPane.showConfirmDialog(
655                    MainApplication.getMainFrame(),
656                    trn(
657                            "<html>The following changeset tag contains an empty key/value:<br>{0}<br>Continue?</html>",
658                            "<html>The following changeset tags contain an empty key/value:<br>{0}<br>Continue?</html>",
659                            emptyChangesetTags.size(), Utils.joinAsHtmlUnorderedList(emptyChangesetTags)),
660                    tr("Empty metadata"),
661                    JOptionPane.OK_CANCEL_OPTION,
662                    JOptionPane.WARNING_MESSAGE
663            )) {
664                dialog.handleMissingComment();
665                return;
666            }
667
668            UploadStrategySpecification strategy = dialog.getUploadStrategySpecification();
669            if (strategy.getStrategy() == UploadStrategy.CHUNKED_DATASET_STRATEGY
670                    && strategy.getChunkSize() == UploadStrategySpecification.UNSPECIFIED_CHUNK_SIZE) {
671                warnIllegalChunkSize();
672                dialog.handleIllegalChunkSize();
673                return;
674            }
675            if (dialog instanceof AbstractUploadDialog) {
676                ((AbstractUploadDialog) dialog).setCanceled(false);
677                ((AbstractUploadDialog) dialog).setVisible(false);
678            }
679        }
680    }
681
682    /**
683     * Action for canceling the dialog.
684     */
685    static class CancelAction extends AbstractAction {
686
687        private final transient IUploadDialog dialog;
688
689        CancelAction(IUploadDialog dialog) {
690            this.dialog = dialog;
691            putValue(NAME, tr("Cancel"));
692            new ImageProvider("cancel").getResource().attachImageIcon(this, true);
693            putValue(SHORT_DESCRIPTION, tr("Cancel the upload and resume editing"));
694        }
695
696        @Override
697        public void actionPerformed(ActionEvent e) {
698            if (dialog instanceof AbstractUploadDialog) {
699                ((AbstractUploadDialog) dialog).setCanceled(true);
700                ((AbstractUploadDialog) dialog).setVisible(false);
701            }
702        }
703    }
704
705    /**
706     * Listens to window closing events and processes them as cancel events.
707     * Listens to window open events and initializes user input
708     */
709    class WindowEventHandler extends WindowAdapter {
710        private boolean activatedOnce;
711
712        @Override
713        public void windowClosing(WindowEvent e) {
714            setCanceled(true);
715        }
716
717        @Override
718        public void windowActivated(WindowEvent e) {
719            if (!activatedOnce && tpConfigPanels.getSelectedIndex() == 0) {
720                pnlBasicUploadSettings.initEditingOfUploadComment();
721                activatedOnce = true;
722            }
723        }
724    }
725
726    /* -------------------------------------------------------------------------- */
727    /* Interface PropertyChangeListener                                           */
728    /* -------------------------------------------------------------------------- */
729    @Override
730    public void propertyChange(PropertyChangeEvent evt) {
731        if (evt.getPropertyName().equals(ChangesetManagementPanel.SELECTED_CHANGESET_PROP)) {
732            Changeset cs = (Changeset) evt.getNewValue();
733            setChangesetTags(dataSet, cs == null); // keep comment/source of first tab for new changesets
734            if (cs == null) {
735                tpConfigPanels.setTitleAt(1, tr("Tags of new changeset"));
736            } else {
737                tpConfigPanels.setTitleAt(1, tr("Tags of changeset {0}", cs.getId()));
738            }
739        }
740    }
741
742    /* -------------------------------------------------------------------------- */
743    /* Interface PreferenceChangedListener                                        */
744    /* -------------------------------------------------------------------------- */
745    @Override
746    public void preferenceChanged(PreferenceChangeEvent e) {
747        if (e.getKey() != null
748                && e.getSource() != getClass()
749                && e.getSource() != BasicUploadSettingsPanel.class) {
750            switch (e.getKey()) {
751                case "osm-server.url":
752                    osmServerUrlChanged(e.getNewValue());
753                    break;
754                case BasicUploadSettingsPanel.HISTORY_KEY:
755                case BasicUploadSettingsPanel.SOURCE_HISTORY_KEY:
756                    pnlBasicUploadSettings.refreshHistoryComboBoxes();
757                    break;
758                default:
759                    return;
760            }
761        }
762    }
763
764    private void osmServerUrlChanged(Setting<?> newValue) {
765        final String url;
766        if (newValue == null || newValue.getValue() == null) {
767            url = OsmApi.getOsmApi().getBaseUrl();
768        } else {
769            url = newValue.getValue().toString();
770        }
771        setTitle(tr("Upload to ''{0}''", url));
772    }
773
774    private static String getLastChangesetTagFromHistory(String historyKey, List<String> def) {
775        Collection<String> history = Config.getPref().getList(historyKey, def);
776        long age = System.currentTimeMillis() / 1000 - Config.getPref().getLong(BasicUploadSettingsPanel.HISTORY_LAST_USED_KEY, 0);
777        if (age < Config.getPref().getLong(BasicUploadSettingsPanel.HISTORY_MAX_AGE_KEY, TimeUnit.HOURS.toSeconds(4))
778                && !history.isEmpty()) {
779            return history.iterator().next();
780        }
781        return null;
782    }
783
784    /**
785     * Returns the last changeset comment from history.
786     * @return the last changeset comment from history
787     */
788    public static String getLastChangesetCommentFromHistory() {
789        return getLastChangesetTagFromHistory(BasicUploadSettingsPanel.HISTORY_KEY, new ArrayList<String>());
790    }
791
792    /**
793     * Returns the last changeset source from history.
794     * @return the last changeset source from history
795     */
796    public static String getLastChangesetSourceFromHistory() {
797        return getLastChangesetTagFromHistory(BasicUploadSettingsPanel.SOURCE_HISTORY_KEY, BasicUploadSettingsPanel.getDefaultSources());
798    }
799
800    @Override
801    public Map<String, String> getTags(boolean keepEmpty) {
802        return pnlTagSettings.getTags(keepEmpty);
803    }
804
805    @Override
806    public void handleMissingComment() {
807        tpConfigPanels.setSelectedIndex(0);
808        pnlBasicUploadSettings.initEditingOfUploadComment();
809    }
810
811    @Override
812    public void handleMissingSource() {
813        tpConfigPanels.setSelectedIndex(0);
814        pnlBasicUploadSettings.initEditingOfUploadSource();
815    }
816
817    @Override
818    public void handleIllegalChunkSize() {
819        tpConfigPanels.setSelectedIndex(0);
820    }
821
822    @Override
823    public void forceUpdateActiveField() {
824        if (tpConfigPanels.getSelectedComponent() == pnlBasicUploadSettings) {
825            pnlBasicUploadSettings.forceUpdateActiveField();
826        }
827    }
828
829    /**
830     * Clean dialog state and release resources.
831     * @since 14251
832     */
833    public void clean() {
834        setUploadedPrimitives(null);
835        dataSet = null;
836    }
837}