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