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