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