001// License: GPL. For details, see LICENSE file. 002package org.openstreetmap.josm.actions; 003 004import static org.openstreetmap.josm.gui.help.HelpUtil.ht; 005import static org.openstreetmap.josm.tools.I18n.tr; 006 007import java.awt.event.ActionEvent; 008import java.awt.event.KeyEvent; 009import java.util.LinkedList; 010import java.util.List; 011import java.util.Map; 012import java.util.Optional; 013 014import javax.swing.JOptionPane; 015 016import org.openstreetmap.josm.actions.upload.ApiPreconditionCheckerHook; 017import org.openstreetmap.josm.actions.upload.DiscardTagsHook; 018import org.openstreetmap.josm.actions.upload.FixDataHook; 019import org.openstreetmap.josm.actions.upload.RelationUploadOrderHook; 020import org.openstreetmap.josm.actions.upload.UploadHook; 021import org.openstreetmap.josm.actions.upload.ValidateUploadHook; 022import org.openstreetmap.josm.data.APIDataSet; 023import org.openstreetmap.josm.data.conflict.ConflictCollection; 024import org.openstreetmap.josm.data.osm.Changeset; 025import org.openstreetmap.josm.gui.HelpAwareOptionPane; 026import org.openstreetmap.josm.gui.MainApplication; 027import org.openstreetmap.josm.gui.Notification; 028import org.openstreetmap.josm.gui.io.AsynchronousUploadPrimitivesTask; 029import org.openstreetmap.josm.gui.io.UploadDialog; 030import org.openstreetmap.josm.gui.io.UploadPrimitivesTask; 031import org.openstreetmap.josm.gui.layer.AbstractModifiableLayer; 032import org.openstreetmap.josm.gui.layer.OsmDataLayer; 033import org.openstreetmap.josm.gui.util.GuiHelper; 034import org.openstreetmap.josm.io.ChangesetUpdater; 035import org.openstreetmap.josm.io.UploadStrategySpecification; 036import org.openstreetmap.josm.spi.preferences.Config; 037import org.openstreetmap.josm.tools.ImageProvider; 038import org.openstreetmap.josm.tools.Logging; 039import org.openstreetmap.josm.tools.Shortcut; 040import org.openstreetmap.josm.tools.Utils; 041 042/** 043 * Action that opens a connection to the osm server and uploads all changes. 044 * 045 * A dialog is displayed asking the user to specify a rectangle to grab. 046 * The url and account settings from the preferences are used. 047 * 048 * If the upload fails this action offers various options to resolve conflicts. 049 * 050 * @author imi 051 */ 052public class UploadAction extends AbstractUploadAction { 053 /** 054 * The list of upload hooks. These hooks will be called one after the other 055 * when the user wants to upload data. Plugins can insert their own hooks here 056 * if they want to be able to veto an upload. 057 * 058 * Be default, the standard upload dialog is the only element in the list. 059 * Plugins should normally insert their code before that, so that the upload 060 * dialog is the last thing shown before upload really starts; on occasion 061 * however, a plugin might also want to insert something after that. 062 */ 063 private static final List<UploadHook> UPLOAD_HOOKS = new LinkedList<>(); 064 private static final List<UploadHook> LATE_UPLOAD_HOOKS = new LinkedList<>(); 065 066 private static final String IS_ASYNC_UPLOAD_ENABLED = "asynchronous.upload"; 067 068 static { 069 /** 070 * Calls validator before upload. 071 */ 072 UPLOAD_HOOKS.add(new ValidateUploadHook()); 073 074 /** 075 * Fixes database errors 076 */ 077 UPLOAD_HOOKS.add(new FixDataHook()); 078 079 /** 080 * Checks server capabilities before upload. 081 */ 082 UPLOAD_HOOKS.add(new ApiPreconditionCheckerHook()); 083 084 /** 085 * Adjusts the upload order of new relations 086 */ 087 UPLOAD_HOOKS.add(new RelationUploadOrderHook()); 088 089 /** 090 * Removes discardable tags like created_by on modified objects 091 */ 092 LATE_UPLOAD_HOOKS.add(new DiscardTagsHook()); 093 } 094 095 /** 096 * Registers an upload hook. Adds the hook at the first position of the upload hooks. 097 * 098 * @param hook the upload hook. Ignored if null. 099 */ 100 public static void registerUploadHook(UploadHook hook) { 101 registerUploadHook(hook, false); 102 } 103 104 /** 105 * Registers an upload hook. Adds the hook at the first position of the upload hooks. 106 * 107 * @param hook the upload hook. Ignored if null. 108 * @param late true, if the hook should be executed after the upload dialog 109 * has been confirmed. Late upload hooks should in general succeed and not 110 * abort the upload. 111 */ 112 public static void registerUploadHook(UploadHook hook, boolean late) { 113 if (hook == null) return; 114 if (late) { 115 if (!LATE_UPLOAD_HOOKS.contains(hook)) { 116 LATE_UPLOAD_HOOKS.add(0, hook); 117 } 118 } else { 119 if (!UPLOAD_HOOKS.contains(hook)) { 120 UPLOAD_HOOKS.add(0, hook); 121 } 122 } 123 } 124 125 /** 126 * Unregisters an upload hook. Removes the hook from the list of upload hooks. 127 * 128 * @param hook the upload hook. Ignored if null. 129 */ 130 public static void unregisterUploadHook(UploadHook hook) { 131 if (hook == null) return; 132 UPLOAD_HOOKS.remove(hook); 133 LATE_UPLOAD_HOOKS.remove(hook); 134 } 135 136 /** 137 * Constructs a new {@code UploadAction}. 138 */ 139 public UploadAction() { 140 super(tr("Upload data..."), "upload", tr("Upload all changes in the active data layer to the OSM server"), 141 Shortcut.registerShortcut("file:upload", tr("File: {0}", tr("Upload data")), KeyEvent.VK_UP, Shortcut.CTRL_SHIFT), true); 142 setHelpId(ht("/Action/Upload")); 143 } 144 145 @Override 146 protected boolean listenToSelectionChange() { 147 return false; 148 } 149 150 @Override 151 protected void updateEnabledState() { 152 OsmDataLayer editLayer = getLayerManager().getEditLayer(); 153 setEnabled(editLayer != null && editLayer.requiresUploadToServer()); 154 } 155 156 /** 157 * Check whether the preconditions are met to upload data from a given layer, if applicable. 158 * @param layer layer to check 159 * @return {@code true} if the preconditions are met, or not applicable 160 * @see #checkPreUploadConditions(AbstractModifiableLayer, APIDataSet) 161 */ 162 public static boolean checkPreUploadConditions(AbstractModifiableLayer layer) { 163 return checkPreUploadConditions(layer, 164 layer instanceof OsmDataLayer ? new APIDataSet(((OsmDataLayer) layer).getDataSet()) : null); 165 } 166 167 protected static void alertUnresolvedConflicts(OsmDataLayer layer) { 168 HelpAwareOptionPane.showOptionDialog( 169 MainApplication.getMainFrame(), 170 tr("<html>The data to be uploaded participates in unresolved conflicts of layer ''{0}''.<br>" 171 + "You have to resolve them first.</html>", Utils.escapeReservedCharactersHTML(layer.getName()) 172 ), 173 tr("Warning"), 174 JOptionPane.WARNING_MESSAGE, 175 ht("/Action/Upload#PrimitivesParticipateInConflicts") 176 ); 177 } 178 179 /** 180 * Warn user about discouraged upload, propose to cancel operation. 181 * @param layer incriminated layer 182 * @return true if the user wants to cancel, false if they want to continue 183 */ 184 public static boolean warnUploadDiscouraged(AbstractModifiableLayer layer) { 185 return GuiHelper.warnUser(tr("Upload discouraged"), 186 "<html>" + 187 tr("You are about to upload data from the layer ''{0}''.<br /><br />"+ 188 "Sending data from this layer is <b>strongly discouraged</b>. If you continue,<br />"+ 189 "it may require you subsequently have to revert your changes, or force other contributors to.<br /><br />"+ 190 "Are you sure you want to continue?", Utils.escapeReservedCharactersHTML(layer.getName()))+ 191 "</html>", 192 ImageProvider.get("upload"), tr("Ignore this hint and upload anyway")); 193 } 194 195 /** 196 * Check whether the preconditions are met to upload data in <code>apiData</code>. 197 * Makes sure upload is allowed, primitives in <code>apiData</code> don't participate in conflicts and 198 * runs the installed {@link UploadHook}s. 199 * 200 * @param layer the source layer of the data to be uploaded 201 * @param apiData the data to be uploaded 202 * @return true, if the preconditions are met; false, otherwise 203 */ 204 public static boolean checkPreUploadConditions(AbstractModifiableLayer layer, APIDataSet apiData) { 205 if (layer.isUploadDiscouraged() && warnUploadDiscouraged(layer)) { 206 return false; 207 } 208 if (layer instanceof OsmDataLayer) { 209 OsmDataLayer osmLayer = (OsmDataLayer) layer; 210 ConflictCollection conflicts = osmLayer.getConflicts(); 211 if (apiData.participatesInConflict(conflicts)) { 212 alertUnresolvedConflicts(osmLayer); 213 return false; 214 } 215 } 216 // Call all upload hooks in sequence. 217 // FIXME: this should become an asynchronous task 218 // 219 if (apiData != null) { 220 return UPLOAD_HOOKS.stream().allMatch(hook -> hook.checkUpload(apiData)); 221 } 222 223 return true; 224 } 225 226 /** 227 * Uploads data to the OSM API. 228 * 229 * @param layer the source layer for the data to upload 230 * @param apiData the primitives to be added, updated, or deleted 231 */ 232 public void uploadData(final OsmDataLayer layer, APIDataSet apiData) { 233 if (apiData.isEmpty()) { 234 new Notification(tr("No changes to upload.")).show(); 235 return; 236 } 237 if (!checkPreUploadConditions(layer, apiData)) 238 return; 239 240 ChangesetUpdater.check(); 241 242 final UploadDialog dialog = UploadDialog.getUploadDialog(); 243 dialog.setUploadedPrimitives(apiData); 244 dialog.initLifeCycle(layer.getDataSet()); 245 dialog.setVisible(true); 246 dialog.rememberUserInput(); 247 if (dialog.isCanceled()) { 248 dialog.clean(); 249 return; 250 } 251 252 for (UploadHook hook : LATE_UPLOAD_HOOKS) { 253 if (!hook.checkUpload(apiData)) { 254 dialog.clean(); 255 return; 256 } 257 } 258 259 // Any hooks want to change the changeset tags? 260 Changeset cs = dialog.getChangeset(); 261 Map<String, String> changesetTags = cs.getKeys(); 262 for (UploadHook hook : UPLOAD_HOOKS) { 263 hook.modifyChangesetTags(changesetTags); 264 } 265 for (UploadHook hook : LATE_UPLOAD_HOOKS) { 266 hook.modifyChangesetTags(changesetTags); 267 } 268 269 UploadStrategySpecification uploadStrategySpecification = dialog.getUploadStrategySpecification(); 270 Logging.info("Starting upload with tags {0}", changesetTags); 271 Logging.info(uploadStrategySpecification.toString()); 272 Logging.info(cs.toString()); 273 dialog.clean(); 274 275 if (Config.getPref().getBoolean(IS_ASYNC_UPLOAD_ENABLED, true)) { 276 Optional<AsynchronousUploadPrimitivesTask> asyncUploadTask = AsynchronousUploadPrimitivesTask.createAsynchronousUploadTask( 277 uploadStrategySpecification, layer, apiData, cs); 278 279 if (asyncUploadTask.isPresent()) { 280 MainApplication.worker.execute(asyncUploadTask.get()); 281 } 282 } else { 283 MainApplication.worker.execute(new UploadPrimitivesTask(uploadStrategySpecification, layer, apiData, cs)); 284 } 285 } 286 287 @Override 288 public void actionPerformed(ActionEvent e) { 289 if (!isEnabled()) 290 return; 291 if (MainApplication.getMap() == null) { 292 new Notification(tr("Nothing to upload. Get some data first.")).show(); 293 return; 294 } 295 APIDataSet apiData = new APIDataSet(getLayerManager().getEditDataSet()); 296 uploadData(getLayerManager().getEditLayer(), apiData); 297 } 298}