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}