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 // This probably fixes #18523. Don't know why. Don't know how. It just does. 436 super.getPreferredSize(); 437 // make sure the tabbed pane never grabs more space than necessary 438 return super.getMinimumSize(); 439 } 440 } 441 442 /** 443 * Handles an upload. 444 */ 445 static class UploadAction extends AbstractAction { 446 447 private final transient IUploadDialog dialog; 448 449 UploadAction(IUploadDialog dialog) { 450 this.dialog = dialog; 451 putValue(NAME, tr("Upload Changes")); 452 new ImageProvider("upload").getResource().attachImageIcon(this, true); 453 putValue(SHORT_DESCRIPTION, tr("Upload the changed primitives")); 454 } 455 456 /** 457 * Displays a warning message indicating that the upload comment is empty/short. 458 * @return true if the user wants to revisit, false if they want to continue 459 */ 460 protected boolean warnUploadComment() { 461 return warnUploadTag( 462 tr("Please revise upload comment"), 463 tr("Your upload comment is <i>empty</i>, or <i>very short</i>.<br /><br />" + 464 "This is technically allowed, but please consider that many users who are<br />" + 465 "watching changes in their area depend on meaningful changeset comments<br />" + 466 "to understand what is going on!<br /><br />" + 467 "If you spend a minute now to explain your change, you will make life<br />" + 468 "easier for many other mappers."), 469 "upload_comment_is_empty_or_very_short" 470 ); 471 } 472 473 /** 474 * Displays a warning message indicating that no changeset source is given. 475 * @return true if the user wants to revisit, false if they want to continue 476 */ 477 protected boolean warnUploadSource() { 478 return warnUploadTag( 479 tr("Please specify a changeset source"), 480 tr("You did not specify a source for your changes.<br />" + 481 "It is technically allowed, but this information helps<br />" + 482 "other users to understand the origins of the data.<br /><br />" + 483 "If you spend a minute now to explain your change, you will make life<br />" + 484 "easier for many other mappers."), 485 "upload_source_is_empty" 486 ); 487 } 488 489 /** 490 * Displays a warning message indicating that the upload comment is rejected. 491 * @param details details explaining why 492 * @return {@code true} 493 */ 494 protected boolean warnRejectedUploadComment(String details) { 495 return warnRejectedUploadTag( 496 tr("Please revise upload comment"), 497 tr("Your upload comment is <i>rejected</i>.") + "<br />" + details 498 ); 499 } 500 501 /** 502 * Displays a warning message indicating that the changeset source is rejected. 503 * @param details details explaining why 504 * @return {@code true} 505 */ 506 protected boolean warnRejectedUploadSource(String details) { 507 return warnRejectedUploadTag( 508 tr("Please revise changeset source"), 509 tr("Your changeset source is <i>rejected</i>.") + "<br />" + details 510 ); 511 } 512 513 /** 514 * Warn about an upload tag with the possibility of resuming the upload. 515 * @param title dialog title 516 * @param message dialog message 517 * @param togglePref preference entry to offer the user a "Do not show again" checkbox for the dialog 518 * @return {@code true} if the user wants to revise the upload tag 519 */ 520 protected boolean warnUploadTag(final String title, final String message, final String togglePref) { 521 return warnUploadTag(title, message, togglePref, true); 522 } 523 524 /** 525 * Warn about an upload tag without the possibility of resuming the upload. 526 * @param title dialog title 527 * @param message dialog message 528 * @return {@code true} 529 */ 530 protected boolean warnRejectedUploadTag(final String title, final String message) { 531 return warnUploadTag(title, message, null, false); 532 } 533 534 private boolean warnUploadTag(final String title, final String message, final String togglePref, boolean allowContinue) { 535 List<String> buttonTexts = new ArrayList<>(Arrays.asList(tr("Revise"), tr("Cancel"))); 536 List<Icon> buttonIcons = new ArrayList<>(Arrays.asList( 537 new ImageProvider("ok").setMaxSize(ImageSizes.LARGEICON).get(), 538 new ImageProvider("cancel").setMaxSize(ImageSizes.LARGEICON).get())); 539 List<String> tooltips = new ArrayList<>(Arrays.asList( 540 tr("Return to the previous dialog to enter a more descriptive comment"), 541 tr("Cancel and return to the previous dialog"))); 542 if (allowContinue) { 543 buttonTexts.add(tr("Continue as is")); 544 buttonIcons.add(new ImageProvider("upload").setMaxSize(ImageSizes.LARGEICON).addOverlay( 545 new ImageOverlay(new ImageProvider("warning-small"), 0.5, 0.5, 1.0, 1.0)).get()); 546 tooltips.add(tr("Ignore this hint and upload anyway")); 547 } 548 549 ExtendedDialog dlg = new ExtendedDialog((Component) dialog, title, buttonTexts.toArray(new String[] {})) { 550 @Override 551 public void setupDialog() { 552 super.setupDialog(); 553 InputMapUtils.addCtrlEnterAction(getRootPane(), buttons.get(buttons.size() - 1).getAction()); 554 } 555 }; 556 dlg.setContent("<html>" + message + "</html>"); 557 dlg.setButtonIcons(buttonIcons.toArray(new Icon[] {})); 558 dlg.setToolTipTexts(tooltips.toArray(new String[] {})); 559 dlg.setIcon(JOptionPane.WARNING_MESSAGE); 560 if (allowContinue) { 561 dlg.toggleEnable(togglePref); 562 } 563 dlg.setCancelButton(1, 2); 564 return dlg.showDialog().getValue() != 3; 565 } 566 567 protected void warnIllegalChunkSize() { 568 HelpAwareOptionPane.showOptionDialog( 569 (Component) dialog, 570 tr("Please enter a valid chunk size first"), 571 tr("Illegal chunk size"), 572 JOptionPane.ERROR_MESSAGE, 573 ht("/Dialog/Upload#IllegalChunkSize") 574 ); 575 } 576 577 static boolean isUploadCommentTooShort(String comment) { 578 String s = Utils.strip(comment); 579 boolean result = true; 580 if (!s.isEmpty()) { 581 UnicodeBlock block = Character.UnicodeBlock.of(s.charAt(0)); 582 if (block != null && block.toString().contains("CJK")) { 583 result = s.length() < 4; 584 } else { 585 result = s.length() < 10; 586 } 587 } 588 return result; 589 } 590 591 private static String lower(String s) { 592 return s.toLowerCase(Locale.ENGLISH); 593 } 594 595 static String validateUploadTag(String uploadValue, String preferencePrefix, 596 List<String> defMandatory, List<String> defForbidden, List<String> defException) { 597 String uploadValueLc = lower(uploadValue); 598 // Check mandatory terms 599 List<String> missingTerms = Config.getPref().getList(preferencePrefix+".mandatory-terms", defMandatory) 600 .stream().map(UploadAction::lower).filter(x -> !uploadValueLc.contains(x)).collect(Collectors.toList()); 601 if (!missingTerms.isEmpty()) { 602 return tr("The following required terms are missing: {0}", missingTerms); 603 } 604 // Check forbidden terms 605 List<String> exceptions = Config.getPref().getList(preferencePrefix+".exception-terms", defException); 606 List<String> forbiddenTerms = Config.getPref().getList(preferencePrefix+".forbidden-terms", defForbidden) 607 .stream().map(UploadAction::lower) 608 .filter(x -> uploadValueLc.contains(x) && exceptions.stream().noneMatch(uploadValueLc::contains)) 609 .collect(Collectors.toList()); 610 if (!forbiddenTerms.isEmpty()) { 611 return tr("The following forbidden terms have been found: {0}", forbiddenTerms); 612 } 613 return null; 614 } 615 616 @Override 617 public void actionPerformed(ActionEvent e) { 618 // force update of model in case dialog is closed before focus lost event, see #17452 619 dialog.forceUpdateActiveField(); 620 621 final List<String> def = Collections.emptyList(); 622 final String uploadComment = dialog.getUploadComment(); 623 final String uploadCommentRejection = validateUploadTag( 624 uploadComment, "upload.comment", def, def, def); 625 if ((isUploadCommentTooShort(uploadComment) && warnUploadComment()) || 626 (uploadCommentRejection != null && warnRejectedUploadComment(uploadCommentRejection))) { 627 // abort for missing or rejected comment 628 dialog.handleMissingComment(); 629 return; 630 } 631 final String uploadSource = dialog.getUploadSource(); 632 final String uploadSourceRejection = validateUploadTag( 633 uploadSource, "upload.source", def, def, def); 634 if ((Utils.isStripEmpty(uploadSource) && warnUploadSource()) || 635 (uploadSourceRejection != null && warnRejectedUploadSource(uploadSourceRejection))) { 636 // abort for missing or rejected changeset source 637 dialog.handleMissingSource(); 638 return; 639 } 640 641 /* test for empty tags in the changeset metadata and proceed only after user's confirmation. 642 * though, accept if key and value are empty (cf. xor). */ 643 List<String> emptyChangesetTags = new ArrayList<>(); 644 for (final Entry<String, String> i : dialog.getTags(true).entrySet()) { 645 final boolean isKeyEmpty = Utils.isStripEmpty(i.getKey()); 646 final boolean isValueEmpty = Utils.isStripEmpty(i.getValue()); 647 final boolean ignoreKey = "comment".equals(i.getKey()) || "source".equals(i.getKey()); 648 if ((isKeyEmpty ^ isValueEmpty) && !ignoreKey) { 649 emptyChangesetTags.add(tr("{0}={1}", i.getKey(), i.getValue())); 650 } 651 } 652 if (!emptyChangesetTags.isEmpty() && JOptionPane.OK_OPTION != JOptionPane.showConfirmDialog( 653 MainApplication.getMainFrame(), 654 trn( 655 "<html>The following changeset tag contains an empty key/value:<br>{0}<br>Continue?</html>", 656 "<html>The following changeset tags contain an empty key/value:<br>{0}<br>Continue?</html>", 657 emptyChangesetTags.size(), Utils.joinAsHtmlUnorderedList(emptyChangesetTags)), 658 tr("Empty metadata"), 659 JOptionPane.OK_CANCEL_OPTION, 660 JOptionPane.WARNING_MESSAGE 661 )) { 662 dialog.handleMissingComment(); 663 return; 664 } 665 666 UploadStrategySpecification strategy = dialog.getUploadStrategySpecification(); 667 if (strategy.getStrategy() == UploadStrategy.CHUNKED_DATASET_STRATEGY 668 && strategy.getChunkSize() == UploadStrategySpecification.UNSPECIFIED_CHUNK_SIZE) { 669 warnIllegalChunkSize(); 670 dialog.handleIllegalChunkSize(); 671 return; 672 } 673 if (dialog instanceof AbstractUploadDialog) { 674 ((AbstractUploadDialog) dialog).setCanceled(false); 675 ((AbstractUploadDialog) dialog).setVisible(false); 676 } 677 } 678 } 679 680 /** 681 * Action for canceling the dialog. 682 */ 683 static class CancelAction extends AbstractAction { 684 685 private final transient IUploadDialog dialog; 686 687 CancelAction(IUploadDialog dialog) { 688 this.dialog = dialog; 689 putValue(NAME, tr("Cancel")); 690 new ImageProvider("cancel").getResource().attachImageIcon(this, true); 691 putValue(SHORT_DESCRIPTION, tr("Cancel the upload and resume editing")); 692 } 693 694 @Override 695 public void actionPerformed(ActionEvent e) { 696 if (dialog instanceof AbstractUploadDialog) { 697 ((AbstractUploadDialog) dialog).setCanceled(true); 698 ((AbstractUploadDialog) dialog).setVisible(false); 699 } 700 } 701 } 702 703 /** 704 * Listens to window closing events and processes them as cancel events. 705 * Listens to window open events and initializes user input 706 */ 707 class WindowEventHandler extends WindowAdapter { 708 private boolean activatedOnce; 709 710 @Override 711 public void windowClosing(WindowEvent e) { 712 setCanceled(true); 713 } 714 715 @Override 716 public void windowActivated(WindowEvent e) { 717 if (!activatedOnce && tpConfigPanels.getSelectedIndex() == 0) { 718 pnlBasicUploadSettings.initEditingOfUploadComment(); 719 activatedOnce = true; 720 } 721 } 722 } 723 724 /* -------------------------------------------------------------------------- */ 725 /* Interface PropertyChangeListener */ 726 /* -------------------------------------------------------------------------- */ 727 @Override 728 public void propertyChange(PropertyChangeEvent evt) { 729 if (evt.getPropertyName().equals(ChangesetManagementPanel.SELECTED_CHANGESET_PROP)) { 730 Changeset cs = (Changeset) evt.getNewValue(); 731 setChangesetTags(dataSet, cs == null); // keep comment/source of first tab for new changesets 732 if (cs == null) { 733 tpConfigPanels.setTitleAt(1, tr("Tags of new changeset")); 734 } else { 735 tpConfigPanels.setTitleAt(1, tr("Tags of changeset {0}", cs.getId())); 736 } 737 } 738 } 739 740 /* -------------------------------------------------------------------------- */ 741 /* Interface PreferenceChangedListener */ 742 /* -------------------------------------------------------------------------- */ 743 @Override 744 public void preferenceChanged(PreferenceChangeEvent e) { 745 if (e.getKey() != null 746 && e.getSource() != getClass() 747 && e.getSource() != BasicUploadSettingsPanel.class) { 748 switch (e.getKey()) { 749 case "osm-server.url": 750 osmServerUrlChanged(e.getNewValue()); 751 break; 752 case BasicUploadSettingsPanel.HISTORY_KEY: 753 case BasicUploadSettingsPanel.SOURCE_HISTORY_KEY: 754 pnlBasicUploadSettings.refreshHistoryComboBoxes(); 755 break; 756 default: 757 return; 758 } 759 } 760 } 761 762 private void osmServerUrlChanged(Setting<?> newValue) { 763 final String url; 764 if (newValue == null || newValue.getValue() == null) { 765 url = OsmApi.getOsmApi().getBaseUrl(); 766 } else { 767 url = newValue.getValue().toString(); 768 } 769 setTitle(tr("Upload to ''{0}''", url)); 770 } 771 772 private static String getLastChangesetTagFromHistory(String historyKey, List<String> def) { 773 Collection<String> history = Config.getPref().getList(historyKey, def); 774 long age = System.currentTimeMillis() / 1000 - BasicUploadSettingsPanel.getHistoryLastUsedKey(); 775 if (age < BasicUploadSettingsPanel.getHistoryMaxAgeKey() && !history.isEmpty()) { 776 return history.iterator().next(); 777 } 778 return null; 779 } 780 781 /** 782 * Returns the last changeset comment from history. 783 * @return the last changeset comment from history 784 */ 785 public static String getLastChangesetCommentFromHistory() { 786 return getLastChangesetTagFromHistory(BasicUploadSettingsPanel.HISTORY_KEY, new ArrayList<String>()); 787 } 788 789 /** 790 * Returns the last changeset source from history. 791 * @return the last changeset source from history 792 */ 793 public static String getLastChangesetSourceFromHistory() { 794 return getLastChangesetTagFromHistory(BasicUploadSettingsPanel.SOURCE_HISTORY_KEY, BasicUploadSettingsPanel.getDefaultSources()); 795 } 796 797 @Override 798 public Map<String, String> getTags(boolean keepEmpty) { 799 return pnlTagSettings.getTags(keepEmpty); 800 } 801 802 @Override 803 public void handleMissingComment() { 804 tpConfigPanels.setSelectedIndex(0); 805 pnlBasicUploadSettings.initEditingOfUploadComment(); 806 } 807 808 @Override 809 public void handleMissingSource() { 810 tpConfigPanels.setSelectedIndex(0); 811 pnlBasicUploadSettings.initEditingOfUploadSource(); 812 } 813 814 @Override 815 public void handleIllegalChunkSize() { 816 tpConfigPanels.setSelectedIndex(0); 817 } 818 819 @Override 820 public void forceUpdateActiveField() { 821 if (tpConfigPanels.getSelectedComponent() == pnlBasicUploadSettings) { 822 pnlBasicUploadSettings.forceUpdateActiveField(); 823 } 824 } 825 826 /** 827 * Clean dialog state and release resources. 828 * @since 14251 829 */ 830 public void clean() { 831 setUploadedPrimitives(null); 832 dataSet = null; 833 } 834}