diff --git a/src/org/openstreetmap/josm/Main.java b/src/org/openstreetmap/josm/Main.java
index 09e5a10..93ace13 100644
--- a/src/org/openstreetmap/josm/Main.java
+++ b/src/org/openstreetmap/josm/Main.java
@@ -83,6 +83,7 @@ import org.openstreetmap.josm.gui.MainMenu;
 import org.openstreetmap.josm.gui.MainPanel;
 import org.openstreetmap.josm.gui.MapFrame;
 import org.openstreetmap.josm.gui.MapFrameListener;
+import org.openstreetmap.josm.gui.datatransfer.OsmTransferHandler;
 import org.openstreetmap.josm.gui.help.HelpUtil;
 import org.openstreetmap.josm.gui.io.SaveLayersDialog;
 import org.openstreetmap.josm.gui.layer.AbstractModifiableLayer;
@@ -177,12 +178,16 @@ public abstract class Main {
 
     /**
      * The global paste buffer.
+     * @deprecated Use swing CCP instead. See {@link OsmTransferHandler}
      */
+    @Deprecated
     public static final PrimitiveDeepCopy pasteBuffer = new PrimitiveDeepCopy();
 
     /**
      * The layer source from which {@link Main#pasteBuffer} data comes from.
+     * @deprecated During a copy operation, the layer should be added. See {@link OsmLayerTransferData}.
      */
+    @Deprecated
     public static Layer pasteSource;
 
     /**
diff --git a/src/org/openstreetmap/josm/actions/CopyAction.java b/src/org/openstreetmap/josm/actions/CopyAction.java
index 6db64bf..e509506 100644
--- a/src/org/openstreetmap/josm/actions/CopyAction.java
+++ b/src/org/openstreetmap/josm/actions/CopyAction.java
@@ -8,25 +8,24 @@ import static org.openstreetmap.josm.tools.I18n.tr;
 import java.awt.event.ActionEvent;
 import java.awt.event.KeyEvent;
 import java.util.Collection;
+import java.util.Collections;
 
 import javax.swing.JOptionPane;
 
 import org.openstreetmap.josm.Main;
+import org.openstreetmap.josm.data.osm.DataSet;
 import org.openstreetmap.josm.data.osm.OsmPrimitive;
-import org.openstreetmap.josm.data.osm.OsmPrimitiveType;
+import org.openstreetmap.josm.gui.datatransfer.ClipboardUtils;
+import org.openstreetmap.josm.gui.datatransfer.PrimitiveTransferable;
+import org.openstreetmap.josm.gui.datatransfer.data.PrimitiveTransferData;
 import org.openstreetmap.josm.gui.layer.OsmDataLayer;
 import org.openstreetmap.josm.tools.Shortcut;
-import org.openstreetmap.josm.tools.Utils;
 
 /**
  * Copy OSM primitives to clipboard in order to paste them, or their tags, somewhere else.
  * @since 404
  */
-public final class CopyAction extends JosmAction {
-
-    /** regular expression that matches text clipboard contents after copying */
-    public static final String CLIPBOARD_REGEXP = "((node|way|relation)\\s\\d+,)*(node|way|relation)\\s\\d+";
-
+public class CopyAction extends JosmAction {
     /**
      * Constructs a new {@code CopyAction}.
      */
@@ -42,8 +41,12 @@ public final class CopyAction extends JosmAction {
 
     @Override
     public void actionPerformed(ActionEvent e) {
-        if (isEmptySelection()) return;
-        Collection<OsmPrimitive> selection = getLayerManager().getEditDataSet().getSelected();
+        DataSet set = getLayerManager().getEditDataSet();
+        Collection<OsmPrimitive> selection = set == null ? Collections.<OsmPrimitive>emptySet() : set.getSelected();
+        if (selection.isEmpty()) {
+            showEmptySelectionWarning();
+            return;
+        }
 
         copy(getLayerManager().getEditLayer(), selection);
     }
@@ -56,19 +59,8 @@ public final class CopyAction extends JosmAction {
      */
     public static void copy(OsmDataLayer source, Collection<OsmPrimitive> primitives) {
         // copy ids to the clipboard
-        String ids = getCopyString(primitives);
-        Utils.copyToClipboard(ids);
-
-        Main.pasteBuffer.makeCopy(primitives);
-        Main.pasteSource = source;
-    }
-
-    static String getCopyString(Collection<? extends OsmPrimitive> primitives) {
-        StringBuilder idsBuilder = new StringBuilder();
-        for (OsmPrimitive p : primitives) {
-            idsBuilder.append(OsmPrimitiveType.from(p).getAPIName()).append(' ').append(p.getId()).append(',');
-        }
-        return idsBuilder.substring(0, idsBuilder.length() - 1);
+        PrimitiveTransferData data = PrimitiveTransferData.getDataWithReferences(primitives);
+        ClipboardUtils.copy(new PrimitiveTransferable(data, source));
     }
 
     @Override
@@ -81,17 +73,12 @@ public final class CopyAction extends JosmAction {
         setEnabled(selection != null && !selection.isEmpty());
     }
 
-    private boolean isEmptySelection() {
-        Collection<OsmPrimitive> sel = getLayerManager().getEditDataSet().getSelected();
-        if (sel.isEmpty()) {
-            JOptionPane.showMessageDialog(
-                    Main.parent,
-                    tr("Please select something to copy."),
-                    tr("Information"),
-                    JOptionPane.INFORMATION_MESSAGE
-            );
-            return true;
-        }
-        return false;
+    protected void showEmptySelectionWarning() {
+        JOptionPane.showMessageDialog(
+                Main.parent,
+                tr("Please select something to copy."),
+                tr("Information"),
+                JOptionPane.INFORMATION_MESSAGE
+        );
     }
 }
diff --git a/src/org/openstreetmap/josm/actions/CopyCoordinatesAction.java b/src/org/openstreetmap/josm/actions/CopyCoordinatesAction.java
index 2394349..f13f2db 100644
--- a/src/org/openstreetmap/josm/actions/CopyCoordinatesAction.java
+++ b/src/org/openstreetmap/josm/actions/CopyCoordinatesAction.java
@@ -11,6 +11,7 @@ import java.util.Collections;
 import org.openstreetmap.josm.data.osm.DataSet;
 import org.openstreetmap.josm.data.osm.Node;
 import org.openstreetmap.josm.data.osm.OsmPrimitive;
+import org.openstreetmap.josm.gui.datatransfer.ClipboardUtils;
 import org.openstreetmap.josm.tools.Shortcut;
 import org.openstreetmap.josm.tools.Utils;
 
@@ -34,7 +35,7 @@ public class CopyCoordinatesAction extends JosmAction {
             s.append(n.getCoor().lon());
             s.append('\n');
         }
-        Utils.copyToClipboard(s.toString().trim());
+        ClipboardUtils.copyString(s.toString().trim());
     }
 
     @Override
diff --git a/src/org/openstreetmap/josm/actions/DuplicateAction.java b/src/org/openstreetmap/josm/actions/DuplicateAction.java
index 488b119..8467c3e 100644
--- a/src/org/openstreetmap/josm/actions/DuplicateAction.java
+++ b/src/org/openstreetmap/josm/actions/DuplicateAction.java
@@ -11,9 +11,14 @@ import java.util.Collection;
 
 import org.openstreetmap.josm.Main;
 import org.openstreetmap.josm.data.osm.OsmPrimitive;
-import org.openstreetmap.josm.data.osm.PrimitiveDeepCopy;
+import org.openstreetmap.josm.gui.datatransfer.OsmTransferHandler;
+import org.openstreetmap.josm.gui.datatransfer.PrimitiveTransferable;
+import org.openstreetmap.josm.gui.datatransfer.data.PrimitiveTransferData;
 import org.openstreetmap.josm.tools.Shortcut;
 
+/**
+ * An action that dupplicates the given nodes. They are not added to the clippboard.
+ */
 public final class DuplicateAction extends JosmAction {
 
     /**
@@ -21,15 +26,15 @@ public final class DuplicateAction extends JosmAction {
      */
     public DuplicateAction() {
         super(tr("Duplicate"), "duplicate",
-                tr("Duplicate selection by copy and immediate paste."),
+                tr("Duplicate selection."),
                 Shortcut.registerShortcut("system:duplicate", tr("Edit: {0}", tr("Duplicate")), KeyEvent.VK_D, Shortcut.CTRL), true);
         putValue("help", ht("/Action/Duplicate"));
     }
 
     @Override
     public void actionPerformed(ActionEvent e) {
-        Main.main.menu.paste.pasteData(
-                new PrimitiveDeepCopy(getLayerManager().getEditDataSet().getSelected()), getLayerManager().getEditLayer(), e);
+        PrimitiveTransferData data = PrimitiveTransferData.getDataWithReferences(getLayerManager().getEditDataSet().getSelected());
+        new OsmTransferHandler().pasteOn(Main.getLayerManager().getEditLayer(), data.getCenter(), new PrimitiveTransferable(data));
     }
 
     @Override
diff --git a/src/org/openstreetmap/josm/actions/MapRectifierWMSmenuAction.java b/src/org/openstreetmap/josm/actions/MapRectifierWMSmenuAction.java
index 12f5bdb..6fb6424 100644
--- a/src/org/openstreetmap/josm/actions/MapRectifierWMSmenuAction.java
+++ b/src/org/openstreetmap/josm/actions/MapRectifierWMSmenuAction.java
@@ -22,12 +22,12 @@ import javax.swing.JRadioButton;
 import org.openstreetmap.josm.Main;
 import org.openstreetmap.josm.data.imagery.ImageryInfo;
 import org.openstreetmap.josm.gui.ExtendedDialog;
+import org.openstreetmap.josm.gui.datatransfer.ClipboardUtils;
 import org.openstreetmap.josm.gui.layer.WMSLayer;
 import org.openstreetmap.josm.gui.widgets.JosmTextField;
 import org.openstreetmap.josm.gui.widgets.UrlLabel;
 import org.openstreetmap.josm.tools.GBC;
 import org.openstreetmap.josm.tools.Shortcut;
-import org.openstreetmap.josm.tools.Utils;
 
 /**
  * Download rectified images from various services.
@@ -123,7 +123,7 @@ public class MapRectifierWMSmenuAction extends JosmAction {
 
         JosmTextField tfWmsUrl = new JosmTextField(30);
 
-        String clip = Utils.getClipboardContent();
+        String clip = ClipboardUtils.getClipboardStringContent();
         clip = clip == null ? "" : clip.trim();
         ButtonGroup group = new ButtonGroup();
 
diff --git a/src/org/openstreetmap/josm/actions/PasteAction.java b/src/org/openstreetmap/josm/actions/PasteAction.java
index d0a1b4b..19f076b 100644
--- a/src/org/openstreetmap/josm/actions/PasteAction.java
+++ b/src/org/openstreetmap/josm/actions/PasteAction.java
@@ -7,33 +7,24 @@ import static org.openstreetmap.josm.tools.I18n.tr;
 
 import java.awt.MouseInfo;
 import java.awt.Point;
+import java.awt.datatransfer.FlavorEvent;
+import java.awt.datatransfer.FlavorListener;
 import java.awt.event.ActionEvent;
 import java.awt.event.KeyEvent;
-import java.util.ArrayList;
-import java.util.HashMap;
-import java.util.List;
-import java.util.Map;
 
 import org.openstreetmap.josm.Main;
-import org.openstreetmap.josm.command.AddPrimitivesCommand;
 import org.openstreetmap.josm.data.coor.EastNorth;
-import org.openstreetmap.josm.data.osm.NodeData;
-import org.openstreetmap.josm.data.osm.OsmPrimitiveType;
-import org.openstreetmap.josm.data.osm.PrimitiveData;
-import org.openstreetmap.josm.data.osm.PrimitiveDeepCopy;
-import org.openstreetmap.josm.data.osm.PrimitiveDeepCopy.PasteBufferChangedListener;
-import org.openstreetmap.josm.data.osm.RelationData;
-import org.openstreetmap.josm.data.osm.RelationMemberData;
-import org.openstreetmap.josm.data.osm.WayData;
-import org.openstreetmap.josm.gui.ExtendedDialog;
-import org.openstreetmap.josm.gui.layer.Layer;
+import org.openstreetmap.josm.gui.datatransfer.ClipboardUtils;
+import org.openstreetmap.josm.gui.datatransfer.OsmTransferHandler;
 import org.openstreetmap.josm.tools.Shortcut;
 
 /**
  * Paste OSM primitives from clipboard to the current edit layer.
  * @since 404
  */
-public final class PasteAction extends JosmAction implements PasteBufferChangedListener {
+public final class PasteAction extends JosmAction implements FlavorListener {
+
+    private final OsmTransferHandler transferHandler;
 
     /**
      * Constructs a new {@code PasteAction}.
@@ -45,59 +36,12 @@ public final class PasteAction extends JosmAction implements PasteBufferChangedL
         // CUA shortcut for paste (https://en.wikipedia.org/wiki/IBM_Common_User_Access#Description)
         Main.registerActionShortcut(this,
                 Shortcut.registerShortcut("system:paste:cua", tr("Edit: {0}", tr("Paste")), KeyEvent.VK_INSERT, Shortcut.SHIFT));
-        Main.pasteBuffer.addPasteBufferChangedListener(this);
+        transferHandler = new OsmTransferHandler();
+        ClipboardUtils.getClipboard().addFlavorListener(this);
     }
 
     @Override
     public void actionPerformed(ActionEvent e) {
-        if (!isEnabled())
-            return;
-        pasteData(Main.pasteBuffer, Main.pasteSource, e);
-    }
-
-    /**
-     * Paste OSM primitives from the given paste buffer and OSM data layer source to the current edit layer.
-     * @param pasteBuffer The paste buffer containing primitive ids to copy
-     * @param source The OSM data layer used to look for primitive ids
-     * @param e The ActionEvent that triggered this operation
-     */
-    public void pasteData(PrimitiveDeepCopy pasteBuffer, Layer source, ActionEvent e) {
-        /* Find the middle of the pasteBuffer area */
-        double maxEast = -1E100;
-        double minEast = 1E100;
-        double maxNorth = -1E100;
-        double minNorth = 1E100;
-        boolean incomplete = false;
-        for (PrimitiveData data : pasteBuffer.getAll()) {
-            if (data instanceof NodeData) {
-                NodeData n = (NodeData) data;
-                if (n.getEastNorth() != null) {
-                    double east = n.getEastNorth().east();
-                    double north = n.getEastNorth().north();
-                    if (east > maxEast) {
-                        maxEast = east;
-                    }
-                    if (east < minEast) {
-                        minEast = east;
-                    }
-                    if (north > maxNorth) {
-                        maxNorth = north;
-                    }
-                    if (north < minNorth) {
-                        minNorth = north;
-                    }
-                }
-            }
-            if (data.isIncomplete()) {
-                incomplete = true;
-            }
-        }
-
-        // Allow to cancel paste if there are incomplete primitives
-        if (incomplete && !confirmDeleteIncomplete()) {
-            return;
-        }
-
         // default to paste in center of map (pasted via menu or cursor not in MapView)
         EastNorth mPosition = Main.map.mapView.getCenter();
         // We previously checked for modifier to know if the action has been trigerred via shortcut or via menu
@@ -112,103 +56,16 @@ public final class PasteAction extends JosmAction implements PasteBufferChangedL
             }
         }
 
-        double offsetEast = mPosition.east() - (maxEast + minEast)/2.0;
-        double offsetNorth = mPosition.north() - (maxNorth + minNorth)/2.0;
-
-        // Make a copy of pasteBuffer and map from old id to copied data id
-        List<PrimitiveData> bufferCopy = new ArrayList<>();
-        List<PrimitiveData> toSelect = new ArrayList<>();
-        Map<Long, Long> newNodeIds = new HashMap<>();
-        Map<Long, Long> newWayIds = new HashMap<>();
-        Map<Long, Long> newRelationIds = new HashMap<>();
-        for (PrimitiveData data: pasteBuffer.getAll()) {
-            if (data.isIncomplete()) {
-                continue;
-            }
-            PrimitiveData copy = data.makeCopy();
-            copy.clearOsmMetadata();
-            if (data instanceof NodeData) {
-                newNodeIds.put(data.getUniqueId(), copy.getUniqueId());
-            } else if (data instanceof WayData) {
-                newWayIds.put(data.getUniqueId(), copy.getUniqueId());
-            } else if (data instanceof RelationData) {
-                newRelationIds.put(data.getUniqueId(), copy.getUniqueId());
-            }
-            bufferCopy.add(copy);
-            if (pasteBuffer.getDirectlyAdded().contains(data)) {
-                toSelect.add(copy);
-            }
-        }
-
-        // Update references in copied buffer
-        for (PrimitiveData data:bufferCopy) {
-            if (data instanceof NodeData) {
-                NodeData nodeData = (NodeData) data;
-                if (Main.getLayerManager().getEditLayer() == source) {
-                    nodeData.setEastNorth(nodeData.getEastNorth().add(offsetEast, offsetNorth));
-                }
-            } else if (data instanceof WayData) {
-                List<Long> newNodes = new ArrayList<>();
-                for (Long oldNodeId: ((WayData) data).getNodes()) {
-                    Long newNodeId = newNodeIds.get(oldNodeId);
-                    if (newNodeId != null) {
-                        newNodes.add(newNodeId);
-                    }
-                }
-                ((WayData) data).setNodes(newNodes);
-            } else if (data instanceof RelationData) {
-                List<RelationMemberData> newMembers = new ArrayList<>();
-                for (RelationMemberData member: ((RelationData) data).getMembers()) {
-                    OsmPrimitiveType memberType = member.getMemberType();
-                    Long newId;
-                    switch (memberType) {
-                    case NODE:
-                        newId = newNodeIds.get(member.getMemberId());
-                        break;
-                    case WAY:
-                        newId = newWayIds.get(member.getMemberId());
-                        break;
-                    case RELATION:
-                        newId = newRelationIds.get(member.getMemberId());
-                        break;
-                    default: throw new AssertionError();
-                    }
-                    if (newId != null) {
-                        newMembers.add(new RelationMemberData(member.getRole(), memberType, newId));
-                    }
-                }
-                ((RelationData) data).setMembers(newMembers);
-            }
-        }
-
-        /* Now execute the commands to add the duplicated contents of the paste buffer to the map */
-        Main.main.undoRedo.add(new AddPrimitivesCommand(bufferCopy, toSelect));
-        Main.map.mapView.repaint();
-    }
-
-    private static boolean confirmDeleteIncomplete() {
-        ExtendedDialog ed = new ExtendedDialog(Main.parent,
-                tr("Delete incomplete members?"),
-                new String[] {tr("Paste without incomplete members"), tr("Cancel")});
-        ed.setButtonIcons(new String[] {"dialogs/relation/deletemembers", "cancel"});
-        ed.setContent(tr("The copied data contains incomplete objects.  "
-                + "When pasting the incomplete objects are removed.  "
-                + "Do you want to paste the data without the incomplete objects?"));
-        ed.showDialog();
-        return ed.getValue() == 1;
+        transferHandler.pasteOn(Main.getLayerManager().getEditLayer(), mPosition);
     }
 
     @Override
     protected void updateEnabledState() {
-        if (getLayerManager().getEditDataSet() == null || Main.pasteBuffer == null) {
-            setEnabled(false);
-            return;
-        }
-        setEnabled(!Main.pasteBuffer.isEmpty());
+        setEnabled(getLayerManager().getEditDataSet() != null && transferHandler.isDataAvailable());
     }
 
     @Override
-    public void pasteBufferChanged(PrimitiveDeepCopy pasteBuffer) {
+    public void flavorsChanged(FlavorEvent e) {
         updateEnabledState();
     }
 }
diff --git a/src/org/openstreetmap/josm/actions/PasteTagsAction.java b/src/org/openstreetmap/josm/actions/PasteTagsAction.java
index 18791ce..027de5d 100644
--- a/src/org/openstreetmap/josm/actions/PasteTagsAction.java
+++ b/src/org/openstreetmap/josm/actions/PasteTagsAction.java
@@ -25,10 +25,10 @@ import org.openstreetmap.josm.data.osm.PrimitiveData;
 import org.openstreetmap.josm.data.osm.Tag;
 import org.openstreetmap.josm.data.osm.TagCollection;
 import org.openstreetmap.josm.gui.conflict.tags.PasteTagsConflictResolverDialog;
+import org.openstreetmap.josm.gui.datatransfer.OsmTransferHandler;
 import org.openstreetmap.josm.tools.I18n;
 import org.openstreetmap.josm.tools.Shortcut;
 import org.openstreetmap.josm.tools.TextTagParser;
-import org.openstreetmap.josm.tools.Utils;
 
 /**
  * Action, to paste all tags from one primitive to another.
@@ -41,6 +41,7 @@ import org.openstreetmap.josm.tools.Utils;
 public final class PasteTagsAction extends JosmAction {
 
     private static final String help = ht("/Action/PasteTags");
+    private final OsmTransferHandler transferHandler = new OsmTransferHandler();
 
     /**
      * Constructs a new {@code PasteTagsAction}.
@@ -53,6 +54,9 @@ public final class PasteTagsAction extends JosmAction {
         putValue("help", help);
     }
 
+    /**
+     * Used to update the tags.
+     */
     public static class TagPaster {
 
         private final Collection<PrimitiveData> source;
@@ -258,13 +262,7 @@ public final class PasteTagsAction extends JosmAction {
         if (selection.isEmpty())
             return;
 
-        String buf = Utils.getClipboardContent();
-        if (buf == null || buf.isEmpty() || buf.matches(CopyAction.CLIPBOARD_REGEXP)) {
-            pasteTagsFromJOSMBuffer(selection);
-        } else {
-            // Paste tags from arbitrary text
-            pasteTagsFromText(selection, buf);
-        }
+        transferHandler.pasteTags(selection);
     }
 
     /**
@@ -292,24 +290,6 @@ public final class PasteTagsAction extends JosmAction {
     }
 
     /**
-     * Paste tags from JOSM buffer
-     * @param selection objects that will have the tags
-     * @return false if JOSM buffer was empty
-     */
-    public static boolean pasteTagsFromJOSMBuffer(Collection<OsmPrimitive> selection) {
-        List<PrimitiveData> directlyAdded = Main.pasteBuffer.getDirectlyAdded();
-        if (directlyAdded == null || directlyAdded.isEmpty()) return false;
-
-        PasteTagsAction.TagPaster tagPaster = new PasteTagsAction.TagPaster(directlyAdded, selection);
-        List<Command> commands = new ArrayList<>();
-        for (Tag tag : tagPaster.execute()) {
-            commands.add(new ChangePropertyCommand(selection, tag.getKey(), "".equals(tag.getValue()) ? null : tag.getValue()));
-        }
-        commitCommands(selection, commands);
-        return true;
-    }
-
-    /**
      * Create and execute SequenceCommand with descriptive title
      * @param selection selected primitives
      * @param commands the commands to perform in a sequential command
diff --git a/src/org/openstreetmap/josm/data/osm/PrimitiveData.java b/src/org/openstreetmap/josm/data/osm/PrimitiveData.java
index 5664dd3..693d277 100644
--- a/src/org/openstreetmap/josm/data/osm/PrimitiveData.java
+++ b/src/org/openstreetmap/josm/data/osm/PrimitiveData.java
@@ -80,6 +80,7 @@ public abstract class PrimitiveData extends AbstractPrimitive implements Seriali
         oos.writeInt(version);
         oos.writeInt(changesetId);
         oos.writeInt(timestamp);
+        oos.writeObject(keys);
         oos.defaultWriteObject();
     }
 
@@ -91,6 +92,7 @@ public abstract class PrimitiveData extends AbstractPrimitive implements Seriali
         version = ois.readInt();
         changesetId = ois.readInt();
         timestamp = ois.readInt();
+        keys = (String[]) ois.readObject();
         ois.defaultReadObject();
     }
 }
diff --git a/src/org/openstreetmap/josm/data/osm/PrimitiveDeepCopy.java b/src/org/openstreetmap/josm/data/osm/PrimitiveDeepCopy.java
index 03db280..3fc21ea 100644
--- a/src/org/openstreetmap/josm/data/osm/PrimitiveDeepCopy.java
+++ b/src/org/openstreetmap/josm/data/osm/PrimitiveDeepCopy.java
@@ -1,35 +1,37 @@
 // License: GPL. For details, see LICENSE file.
 package org.openstreetmap.josm.data.osm;
 
+import java.awt.datatransfer.UnsupportedFlavorException;
+import java.io.IOException;
 import java.util.ArrayList;
 import java.util.Collection;
-import java.util.HashSet;
+import java.util.Collections;
 import java.util.List;
-import java.util.Set;
-import java.util.concurrent.CopyOnWriteArrayList;
 
-import org.openstreetmap.josm.data.osm.visitor.AbstractVisitor;
+import org.openstreetmap.josm.gui.datatransfer.ClipboardUtils;
+import org.openstreetmap.josm.gui.datatransfer.OsmTransferHandler;
+import org.openstreetmap.josm.gui.datatransfer.PrimitiveTransferable;
+import org.openstreetmap.josm.gui.datatransfer.data.PrimitiveTransferData;
 
 /**
  * This class allows to create and keep a deep copy of primitives. Provides methods to access directly added
  * primitives and reference primitives
+ * <p>
+ * To be removed end of 2016
  * @since 2305
+ * @deprecated This has been replaced by Swing Copy+Paste support. Use {@link OsmTransferHandler} instead.
  */
+@Deprecated
 public class PrimitiveDeepCopy {
 
     public interface PasteBufferChangedListener {
         void pasteBufferChanged(PrimitiveDeepCopy pasteBuffer);
     }
 
-    private final List<PrimitiveData> directlyAdded = new ArrayList<>();
-    private final List<PrimitiveData> referenced = new ArrayList<>();
-    private final CopyOnWriteArrayList<PasteBufferChangedListener> listeners = new CopyOnWriteArrayList<>();
-
     /**
      * Constructs a new {@code PrimitiveDeepCopy} without data. Use {@link #makeCopy(Collection)} after that.
      */
     public PrimitiveDeepCopy() {
-        // Do nothing
     }
 
     /**
@@ -45,88 +47,68 @@ public class PrimitiveDeepCopy {
      * Replace content of the object with copy of provided primitives.
      * @param primitives OSM primitives to copy
      * @since 7961
+     * @deprecated Call {@link OsmTransferHandler#copyToClippboard(PrimitiveTransferData)} yourself
      */
+    @Deprecated
     public final void makeCopy(final Collection<? extends OsmPrimitive> primitives) {
-        directlyAdded.clear();
-        referenced.clear();
-
-        final Set<Long> visitedNodeIds = new HashSet<>();
-        final Set<Long> visitedWayIds = new HashSet<>();
-        final Set<Long> visitedRelationIds = new HashSet<>();
-
-        new AbstractVisitor() {
-            private boolean firstIteration;
-
-            @Override
-            public void visit(Node n) {
-                if (!visitedNodeIds.add(n.getUniqueId()))
-                    return;
-                (firstIteration ? directlyAdded : referenced).add(n.save());
-            }
-
-            @Override
-            public void visit(Way w) {
-                if (!visitedWayIds.add(w.getUniqueId()))
-                    return;
-                (firstIteration ? directlyAdded : referenced).add(w.save());
-                firstIteration = false;
-                for (Node n : w.getNodes()) {
-                    visit(n);
-                }
-            }
-
-            @Override
-            public void visit(Relation r) {
-                if (!visitedRelationIds.add(r.getUniqueId()))
-                    return;
-                (firstIteration ? directlyAdded : referenced).add(r.save());
-                firstIteration = false;
-                for (RelationMember m : r.getMembers()) {
-                    m.getMember().accept(this);
-                }
-            }
-
-            public void visitAll() {
-                for (OsmPrimitive osm : primitives) {
-                    firstIteration = true;
-                    osm.accept(this);
-                }
-            }
-        }.visitAll();
-
-        firePasteBufferChanged();
+        ClipboardUtils.copy(new PrimitiveTransferable(PrimitiveTransferData.getDataWithReferences(primitives)));
     }
 
+    /**
+     * Gets the list of primitives that were explicitly added to this copy.
+     * @return The added primitives
+     */
     public List<PrimitiveData> getDirectlyAdded() {
-        return directlyAdded;
+        try {
+            PrimitiveTransferData data = (PrimitiveTransferData) ClipboardUtils.getClipboard().getData(PrimitiveTransferData.DATA_FLAVOR);
+            return new ArrayList<>(data.getDirectlyAdded());
+        } catch (UnsupportedFlavorException | IOException e) {
+            return Collections.emptyList();
+        }
     }
 
+    /**
+     * Gets the list of primitives that were implicitly added because they were referenced.
+     * @return The primitives
+     */
     public List<PrimitiveData> getReferenced() {
-        return referenced;
+        try {
+            PrimitiveTransferData data = (PrimitiveTransferData) ClipboardUtils.getClipboard().getData(PrimitiveTransferData.DATA_FLAVOR);
+            return new ArrayList<>(data.getReferenced());
+        } catch (UnsupportedFlavorException | IOException e) {
+            return Collections.emptyList();
+        }
     }
 
+    /**
+     * Gets a list of all primitives in this copy.
+     * @return The primitives
+     * @see #getDirectlyAdded()
+     * @see #getReferenced()
+     */
     public List<PrimitiveData> getAll() {
-        List<PrimitiveData> result = new ArrayList<>(directlyAdded.size() + referenced.size());
-        result.addAll(directlyAdded);
-        result.addAll(referenced);
-        return result;
+        try {
+            PrimitiveTransferData data = (PrimitiveTransferData) ClipboardUtils.getClipboard().getData(PrimitiveTransferData.DATA_FLAVOR);
+            return new ArrayList<>(data.getAll());
+        } catch (UnsupportedFlavorException | IOException e) {
+            return Collections.emptyList();
+        }
     }
 
     public boolean isEmpty() {
-        return directlyAdded.isEmpty() && referenced.isEmpty();
-    }
-
-    private void firePasteBufferChanged() {
-        for (PasteBufferChangedListener listener: listeners) {
-            listener.pasteBufferChanged(this);
-        }
+        return !ClipboardUtils.getClipboard().isDataFlavorAvailable(PrimitiveTransferData.DATA_FLAVOR);
     }
 
+    /**
+     * Deactivated. To be removed as soon as we think nobody uses it.
+     * @param listener
+     * @deprecated You can detect buffer changes by registering a listener on {@link OsmTransferHandler#getClipboard()}
+     */
+    @Deprecated
     public void addPasteBufferChangedListener(PasteBufferChangedListener listener) {
-        listeners.addIfAbsent(listener);
     }
 
+    @Deprecated
     public void removePasteBufferChangedListener(PasteBufferChangedListener listener) {
-        listeners.remove(listener);
     }
 }
diff --git a/src/org/openstreetmap/josm/data/osm/TagMap.java b/src/org/openstreetmap/josm/data/osm/TagMap.java
index ce49e40..7f413ca 100644
--- a/src/org/openstreetmap/josm/data/osm/TagMap.java
+++ b/src/org/openstreetmap/josm/data/osm/TagMap.java
@@ -1,11 +1,15 @@
 // License: GPL. For details, see LICENSE file.
 package org.openstreetmap.josm.data.osm;
 
+import java.io.Serializable;
 import java.util.AbstractMap;
 import java.util.AbstractSet;
+import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.ConcurrentModificationException;
 import java.util.Iterator;
+import java.util.List;
+import java.util.Map;
 import java.util.NoSuchElementException;
 import java.util.Set;
 
@@ -16,7 +20,8 @@ import java.util.Set;
  *
  * @author Michael Zangl
  */
-public class TagMap extends AbstractMap<String, String> {
+public class TagMap extends AbstractMap<String, String> implements Serializable {
+    static final long serialVersionUID = 1;
     /**
      * We use this array every time we want to represent an empty map.
      * This saves us the burden of checking for null every time but saves some object allocations.
@@ -107,7 +112,23 @@ public class TagMap extends AbstractMap<String, String> {
      * Creates a new, empty tag map.
      */
     public TagMap() {
-        this(null);
+        this((String[]) null);
+    }
+
+    /**
+     * Create a new tag map and load it from the other map.
+     * @param tags The map to load from.
+     */
+    public TagMap(Map<String, String> tags) {
+        putAll(tags);
+    }
+
+    /**
+     * Copy constructor
+     * @param tagMap The map to copy from.
+     */
+    public TagMap(TagMap tagMap) {
+        this(tagMap.tags);
     }
 
     /**
@@ -209,6 +230,19 @@ public class TagMap extends AbstractMap<String, String> {
     }
 
     /**
+     * Gets a list of all tags contained in this map.
+     * @return The list of tags in the order they were added.
+     */
+    public List<Tag> getTags() {
+        String[] tags = this.tags;
+        List<Tag> tagList = new ArrayList<>();
+        for (int i = 0; i < tags.length; i+=2) {
+            tagList.add(new Tag(tags[i], tags[i+1]));
+        }
+        return tagList;
+    }
+
+    /**
      * Finds a key in an array that is structured like the {@link #tags} array and returns the position.
      * <p>
      * We allow the parameter to be passed to allow for better synchronization.
diff --git a/src/org/openstreetmap/josm/gui/MapFrame.java b/src/org/openstreetmap/josm/gui/MapFrame.java
index e3ab5cc..80ce939 100644
--- a/src/org/openstreetmap/josm/gui/MapFrame.java
+++ b/src/org/openstreetmap/josm/gui/MapFrame.java
@@ -8,7 +8,6 @@ import java.awt.Component;
 import java.awt.Container;
 import java.awt.Dimension;
 import java.awt.Font;
-import java.awt.GraphicsEnvironment;
 import java.awt.GridBagLayout;
 import java.awt.Rectangle;
 import java.awt.event.ActionEvent;
@@ -196,9 +195,6 @@ public class MapFrame extends JPanel implements Destroyable, ActiveLayerChangeLi
         setLayout(new BorderLayout());
 
         mapView = new MapView(Main.getLayerManager(), contentPane, viewportData);
-        if (!GraphicsEnvironment.isHeadless()) {
-            new FileDrop(mapView);
-        }
 
         splitPane = new JSplitPane(JSplitPane.HORIZONTAL_SPLIT, true);
 
diff --git a/src/org/openstreetmap/josm/gui/MapView.java b/src/org/openstreetmap/josm/gui/MapView.java
index da3079e..c20ee16 100644
--- a/src/org/openstreetmap/josm/gui/MapView.java
+++ b/src/org/openstreetmap/josm/gui/MapView.java
@@ -52,6 +52,7 @@ import org.openstreetmap.josm.data.osm.visitor.paint.PaintColors;
 import org.openstreetmap.josm.data.osm.visitor.paint.Rendering;
 import org.openstreetmap.josm.data.osm.visitor.paint.relations.MultipolygonCache;
 import org.openstreetmap.josm.gui.MapViewState.MapViewRectangle;
+import org.openstreetmap.josm.gui.datatransfer.OsmTransferHandler;
 import org.openstreetmap.josm.gui.layer.AbstractMapViewPaintable;
 import org.openstreetmap.josm.gui.layer.GpxLayer;
 import org.openstreetmap.josm.gui.layer.ImageryLayer;
@@ -579,6 +580,7 @@ LayerManager.LayerChangeListener, MainLayerManager.ActiveLayerChangeListener {
         for (JComponent c : getMapNavigationComponents(MapView.this)) {
             add(c);
         }
+        setTransferHandler(new OsmTransferHandler());
     }
 
     /**
diff --git a/src/org/openstreetmap/josm/gui/datatransfer/ClipboardUtils.java b/src/org/openstreetmap/josm/gui/datatransfer/ClipboardUtils.java
new file mode 100644
index 0000000..f320a99
--- /dev/null
+++ b/src/org/openstreetmap/josm/gui/datatransfer/ClipboardUtils.java
@@ -0,0 +1,156 @@
+// License: GPL. For details, see LICENSE file.
+package org.openstreetmap.josm.gui.datatransfer;
+
+import java.awt.GraphicsEnvironment;
+import java.awt.HeadlessException;
+import java.awt.Toolkit;
+import java.awt.datatransfer.Clipboard;
+import java.awt.datatransfer.ClipboardOwner;
+import java.awt.datatransfer.DataFlavor;
+import java.awt.datatransfer.StringSelection;
+import java.awt.datatransfer.Transferable;
+import java.awt.datatransfer.UnsupportedFlavorException;
+import java.io.IOException;
+import java.util.concurrent.Callable;
+
+import org.openstreetmap.josm.Main;
+import org.openstreetmap.josm.gui.util.GuiHelper;
+import org.openstreetmap.josm.tools.Utils;
+
+/**
+ * This is a utility class that provides methods useful for general data transfer support.
+ *
+ * @author Michael Zangl
+ * @since xxx
+ */
+public final class ClipboardUtils {
+    private static final class DoNothingClipboardOwner implements ClipboardOwner {
+        @Override
+        public void lostOwnership(Clipboard clpbrd, Transferable t) {
+            // Do nothing
+        }
+    }
+
+    private static Clipboard clipboard;
+
+    private ClipboardUtils() {
+    }
+
+    /**
+     * This method should be used from all of JOSM to access the clipboard.
+     * <p>
+     * It will default to the system clipboard except for cases where that clipboard is not accessible.
+     * @return A clipboard.
+     * @see #getClipboardContent()
+     */
+    public static synchronized Clipboard getClipboard() {
+        // Might be unsupported in some more cases, we need a fake clipboard then.
+        if (clipboard == null) {
+            try {
+                clipboard = Toolkit.getDefaultToolkit().getSystemClipboard();
+            } catch (HeadlessException e) {
+                Main.warn("Headless. Using fake clipboard.", e);
+                clipboard = new Clipboard("fake");
+            }
+        }
+        return clipboard;
+    }
+
+    /**
+     * Gets the singleton instance of the system selection as a <code>Clipboard</code> object.
+     * This allows an application to read and modify the current, system-wide selection.
+     * @return the system selection as a <code>Clipboard</code>, or <code>null</code> if the native platform does not
+     *         support a system selection <code>Clipboard</code> or if GraphicsEnvironment.isHeadless() returns true
+     * @see Toolkit#getSystemSelection
+     * @since xxx
+     */
+    public static Clipboard getSystemSelection() {
+        if (GraphicsEnvironment.isHeadless()) {
+            return null;
+        } else {
+            return Toolkit.getDefaultToolkit().getSystemSelection();
+        }
+    }
+
+    /**
+     * Gets the clipboard content as string.
+     * @return the content if available, <code>null</code> otherwise.
+     */
+    public static String getClipboardStringContent() {
+        try {
+            Transferable t = getClipboardContent();
+            if (t != null && t.isDataFlavorSupported(DataFlavor.stringFlavor)) {
+                return (String) t.getTransferData(DataFlavor.stringFlavor);
+            }
+        } catch (UnsupportedFlavorException | IOException ex) {
+            Main.error(ex);
+        }
+        return null;
+    }
+
+    /**
+     * Extracts clipboard content as {@code Transferable} object. Using this method avoids some problems on some platforms.
+     * @return The content or <code>null</code> if it is not available
+     */
+    public static synchronized Transferable getClipboardContent() {
+        return getClipboardContent(getClipboard());
+    }
+
+    /**
+     * Extracts clipboard content as {@code Transferable} object. Using this method avoids some problems on some platforms.
+     * @param clipboard clipboard from which contents are retrieved
+     * @return clipboard contents if available, {@code null} otherwise.
+     * @since xx
+     */
+    public static Transferable getClipboardContent(Clipboard clipboard) {
+        Transferable t = null;
+        for (int tries = 0; t == null && tries < 10; tries++) {
+            try {
+                t = clipboard.getContents(null);
+            } catch (IllegalStateException e) {
+                // Clipboard currently unavailable.
+                // On some platforms, the system clipboard is unavailable while it is accessed by another application.
+                Main.trace("Clipboard unavailable.", e);
+                try {
+                    Thread.sleep(1);
+                } catch (InterruptedException ex) {
+                    Main.warn("InterruptedException in " + Utils.class.getSimpleName()
+                            + " while getting clipboard content");
+                }
+            } catch (NullPointerException e) {
+                // JDK-6322854: On Linux/X11, NPE can happen for unknown reasons, on all versions of Java
+                Main.error(e);
+            }
+        }
+        return t;
+    }
+
+    /**
+     * Copy the given string to the clipboard.
+     * @param s The string to copy.
+     * @return True if the  copy was successful
+     */
+    public static boolean copyString(String s) {
+        return copy(new StringSelection(s));
+    }
+
+    /**
+     * Copies the given transferable to the clipboard. Handles state problems that occur on some platforms.
+     * @param transferable The transferable to copy.
+     * @return True if the copy was successful
+     */
+    public static boolean copy(final Transferable transferable) {
+        return GuiHelper.runInEDTAndWaitAndReturn(new Callable<Boolean>() {
+            @Override
+            public Boolean call() throws Exception {
+                try {
+                    getClipboard().setContents(transferable, new DoNothingClipboardOwner());
+                    return true;
+                } catch (IllegalStateException ex) {
+                    Main.error(ex);
+                    return false;
+                }
+            }
+        });
+    }
+}
diff --git a/src/org/openstreetmap/josm/gui/datatransfer/OsmTransferHandler.java b/src/org/openstreetmap/josm/gui/datatransfer/OsmTransferHandler.java
new file mode 100644
index 0000000..9f47fd2
--- /dev/null
+++ b/src/org/openstreetmap/josm/gui/datatransfer/OsmTransferHandler.java
@@ -0,0 +1,122 @@
+// License: GPL. For details, see LICENSE file.
+package org.openstreetmap.josm.gui.datatransfer;
+
+import java.awt.datatransfer.DataFlavor;
+import java.awt.datatransfer.Transferable;
+import java.awt.datatransfer.UnsupportedFlavorException;
+import java.io.IOException;
+import java.util.Arrays;
+import java.util.Collection;
+
+import javax.swing.TransferHandler;
+
+import org.openstreetmap.josm.Main;
+import org.openstreetmap.josm.data.coor.EastNorth;
+import org.openstreetmap.josm.data.osm.OsmPrimitive;
+import org.openstreetmap.josm.gui.datatransfer.importers.AbstractOsmDataPaster;
+import org.openstreetmap.josm.gui.datatransfer.importers.FilePaster;
+import org.openstreetmap.josm.gui.datatransfer.importers.PrimitiveDataPaster;
+import org.openstreetmap.josm.gui.datatransfer.importers.TagTransferPaster;
+import org.openstreetmap.josm.gui.datatransfer.importers.TextTagPaster;
+import org.openstreetmap.josm.gui.layer.OsmDataLayer;
+
+/**
+ * This transfer hanlder provides the ability to transfer OSM data. It allows you to receive files, primitives or tags.
+ * @author Michael Zangl
+ * @since xxx
+ */
+public class OsmTransferHandler extends TransferHandler {
+
+    private static final Collection<AbstractOsmDataPaster> SUPPORTED = Arrays.asList(
+            new FilePaster(), new PrimitiveDataPaster(),
+            new TagTransferPaster(), new TextTagPaster());
+
+    @Override
+    public boolean canImport(TransferSupport support) {
+        // import everything for now, only support copy.
+        for (AbstractOsmDataPaster df : SUPPORTED) {
+            if (df.supports(support)) {
+                return true;
+            }
+        }
+        return false;
+    }
+
+    @Override
+    public boolean importData(TransferSupport support) {
+        return importData(support, Main.getLayerManager().getEditLayer(), null);
+    }
+
+    private boolean importData(TransferSupport support, OsmDataLayer layer, EastNorth center) {
+        for (AbstractOsmDataPaster df : SUPPORTED) {
+            if (df.supports(support)) {
+                try {
+                    if (df.importData(support, layer, center)) {
+                        return true;
+                    }
+                } catch (UnsupportedFlavorException | IOException e) {
+                    Main.warn(e);
+                }
+            }
+        }
+        return super.importData(support);
+    }
+
+    private boolean importTags(TransferSupport support, Collection<? extends OsmPrimitive> primitives) {
+        for (AbstractOsmDataPaster df : SUPPORTED) {
+            if (df.supports(support)) {
+                try {
+                    if (df.importTagsOn(support, primitives)) {
+                        return true;
+                    }
+                } catch (UnsupportedFlavorException | IOException e) {
+                    Main.warn(e);
+                }
+            }
+        }
+        return super.importData(support);
+    }
+
+    /**
+     * Paste the current clippboard current at the given position
+     * @param editLayer The layer to paste on.
+     * @param mPosition The position to paste at.
+     */
+    public void pasteOn(OsmDataLayer editLayer, EastNorth mPosition) {
+        Transferable transferable = ClipboardUtils.getClipboard().getContents(null);
+        pasteOn(editLayer, mPosition, transferable);
+    }
+
+    /**
+     * Paste the given clippboard current at the given position
+     * @param editLayer The layer to paste on.
+     * @param mPosition The position to paste at.
+     * @param transferable The transferable to use.
+     */
+    public void pasteOn(OsmDataLayer editLayer, EastNorth mPosition, Transferable transferable) {
+        importData(new TransferSupport(Main.panel, transferable), editLayer, mPosition);
+    }
+
+    /**
+     * Paste the given tags on the primitives.
+     * @param primitives The primitives to paste on.
+     */
+    public void pasteTags(Collection<? extends OsmPrimitive> primitives) {
+        Transferable transferable = ClipboardUtils.getClipboard().getContents(null);
+        importTags(new TransferSupport(Main.panel, transferable), primitives);
+    }
+
+    /**
+     * Check if any primitive data or any other supported data is available in the clippboard.
+     * @return <code>true</code> if any flavor is supported.
+     */
+    public boolean isDataAvailable() {
+        Collection<DataFlavor> available = Arrays.asList(ClipboardUtils.getClipboard().getAvailableDataFlavors());
+        for (AbstractOsmDataPaster s : SUPPORTED) {
+            if (s.supports(available)) {
+                return true;
+            }
+        }
+        return false;
+    }
+}
diff --git a/src/org/openstreetmap/josm/gui/datatransfer/PrimitiveTransferable.java b/src/org/openstreetmap/josm/gui/datatransfer/PrimitiveTransferable.java
index cd91a24..21e302a 100644
--- a/src/org/openstreetmap/josm/gui/datatransfer/PrimitiveTransferable.java
+++ b/src/org/openstreetmap/josm/gui/datatransfer/PrimitiveTransferable.java
@@ -4,92 +4,87 @@ package org.openstreetmap.josm.gui.datatransfer;
 import java.awt.datatransfer.DataFlavor;
 import java.awt.datatransfer.Transferable;
 import java.awt.datatransfer.UnsupportedFlavorException;
-import java.io.Serializable;
 import java.util.ArrayList;
-import java.util.Collection;
+import java.util.Arrays;
+import java.util.List;
 
-import org.openstreetmap.josm.data.osm.OsmPrimitive;
+import org.openstreetmap.josm.data.osm.OsmPrimitiveType;
 import org.openstreetmap.josm.data.osm.PrimitiveData;
-import org.openstreetmap.josm.gui.DefaultNameFormatter;
-import org.openstreetmap.josm.tools.CheckParameterUtil;
+import org.openstreetmap.josm.gui.datatransfer.data.PrimitiveTransferData;
+import org.openstreetmap.josm.gui.datatransfer.data.TagTransferData;
+import org.openstreetmap.josm.gui.layer.OsmDataLayer;
 
 /**
- * Transferable objects for {@link PrimitiveData}.
+ * Transferable objects for {@link PrimitiveTransferData} objects
  * @since 9369
+ * @since xxx Complete rework
  */
 public class PrimitiveTransferable implements Transferable {
 
     /**
-     * A wrapper for a collection of {@link PrimitiveData}.
+     * The flavors that are available for normal primitives.
      */
-    public static final class Data implements Serializable {
-        private static final long serialVersionUID = -1485089993600213704L;
-        private final Collection<PrimitiveData> primitiveData;
-
-        private Data(Collection<PrimitiveData> primitiveData) {
-            CheckParameterUtil.ensureThat(primitiveData instanceof Serializable, "primitiveData must be instanceof Serializable");
-            this.primitiveData = primitiveData;
-        }
-
-        /**
-         * Returns the contained {@link PrimitiveData}
-         * @return the contained {@link PrimitiveData}
-         */
-        public Collection<PrimitiveData> getPrimitiveData() {
-            return primitiveData;
-        }
-    }
+    private static final List<DataFlavor> PRIMITIVE_FLAVORS = Arrays.asList(PrimitiveTransferData.DATA_FLAVOR,
+            TagTransferData.FLAVOR, DataFlavor.stringFlavor);
+    private final PrimitiveTransferData primitives;
+    private OsmDataLayer sourceLayer;
 
     /**
-     * Data flavor for {@link PrimitiveData} which is wrapped in {@link Data}.
+     * Constructs a new {@code PrimitiveTransferable}.
+     * @param primitives collection of OSM primitives
      */
-    public static final DataFlavor PRIMITIVE_DATA = new DataFlavor(Data.class, Data.class.getName());
-    private final Collection<? extends OsmPrimitive> primitives;
+    public PrimitiveTransferable(PrimitiveTransferData primitives) {
+        this(primitives, null);
+    }
 
     /**
      * Constructs a new {@code PrimitiveTransferable}.
      * @param primitives collection of OSM primitives
+     * @param sourceLayer The layer the primitives are copied from.
      */
-    public PrimitiveTransferable(Collection<? extends OsmPrimitive> primitives) {
+    public PrimitiveTransferable(PrimitiveTransferData primitives, OsmDataLayer sourceLayer) {
         this.primitives = primitives;
+        this.sourceLayer = sourceLayer;
     }
 
     @Override
     public DataFlavor[] getTransferDataFlavors() {
-        return new DataFlavor[]{PRIMITIVE_DATA, DataFlavor.stringFlavor};
+        ArrayList<DataFlavor> flavors = new ArrayList<>(PRIMITIVE_FLAVORS);
+        return flavors.toArray(new DataFlavor[flavors.size()]);
     }
 
     @Override
     public boolean isDataFlavorSupported(DataFlavor flavor) {
-        return flavor == PRIMITIVE_DATA;
+        DataFlavor[] flavors = getTransferDataFlavors();
+        for (DataFlavor f : flavors) {
+            if (flavor.equals(f)) {
+                return true;
+            }
+        }
+        return false;
     }
 
     @Override
     public Object getTransferData(DataFlavor flavor) throws UnsupportedFlavorException {
         if (DataFlavor.stringFlavor.equals(flavor)) {
             return getStringData();
-        } else if (PRIMITIVE_DATA.equals(flavor)) {
-            return getPrimitiveData();
+        } else if (PrimitiveTransferData.DATA_FLAVOR.equals(flavor)) {
+            return primitives;
+        } else if (TagTransferData.FLAVOR.equals(flavor)) {
+            return new TagTransferData(primitives.getDirectlyAdded());
+        } else {
+            throw new UnsupportedFlavorException(flavor);
         }
-        throw new UnsupportedFlavorException(flavor);
     }
 
     protected String getStringData() {
         final StringBuilder sb = new StringBuilder();
-        for (OsmPrimitive primitive : primitives) {
-            sb.append(primitive.getType())
-              .append(' ').append(primitive.getUniqueId())
-              .append(" # ").append(primitive.getDisplayName(DefaultNameFormatter.getInstance()))
-              .append('\n');
+        for (PrimitiveData primitive : primitives.getAll()) {
+            if (sb.length() > 0) {
+                sb.append("\n");
+            }
+            sb.append(OsmPrimitiveType.from(primitive).getAPIName()).append(' ').append(primitive.getId());
         }
         return sb.toString().replace("\u200E", "").replace("\u200F", "");
     }
-
-    protected Data getPrimitiveData() {
-        final Collection<PrimitiveData> r = new ArrayList<>(primitives.size());
-        for (OsmPrimitive primitive : primitives) {
-            r.add(primitive.save());
-        }
-        return new Data(r);
-    }
 }
diff --git a/src/org/openstreetmap/josm/gui/datatransfer/RelationMemberTransferable.java b/src/org/openstreetmap/josm/gui/datatransfer/RelationMemberTransferable.java
index 46dc5d2..f7b78c1 100644
--- a/src/org/openstreetmap/josm/gui/datatransfer/RelationMemberTransferable.java
+++ b/src/org/openstreetmap/josm/gui/datatransfer/RelationMemberTransferable.java
@@ -6,11 +6,16 @@ import java.awt.datatransfer.Transferable;
 import java.awt.datatransfer.UnsupportedFlavorException;
 import java.io.Serializable;
 import java.util.ArrayList;
+import java.util.Arrays;
 import java.util.Collection;
+import java.util.Collections;
+import java.util.HashSet;
 
+import org.openstreetmap.josm.data.osm.OsmPrimitive;
 import org.openstreetmap.josm.data.osm.RelationMember;
 import org.openstreetmap.josm.data.osm.RelationMemberData;
 import org.openstreetmap.josm.gui.DefaultNameFormatter;
+import org.openstreetmap.josm.gui.datatransfer.data.PrimitiveTransferData;
 import org.openstreetmap.josm.tools.CheckParameterUtil;
 
 /**
@@ -36,14 +41,27 @@ public class RelationMemberTransferable implements Transferable {
          * @return the contained {@link RelationMemberData}
          */
         public Collection<RelationMemberData> getRelationMemberData() {
-            return relationMemberDatas;
+            return Collections.unmodifiableCollection(relationMemberDatas);
+        }
+
+        /**
+         * Gets the Data for the given list of members.
+         * @param members The collection. The order is preserved.
+         * @return The data.
+         */
+        public static Data getData(Collection<RelationMember> members) {
+            final Collection<RelationMemberData> r = new ArrayList<>(members.size());
+            for (RelationMember member : members) {
+                r.add(new RelationMemberData(member.getRole(), member.getType(), member.getUniqueId()));
+            }
+            return new Data(r);
         }
     }
 
     /**
      * Data flavor for {@link RelationMemberData} which is wrapped in {@link Data}.
      */
-    public static final DataFlavor RELATION_MEMBER_DATA = new DataFlavor(Data.class, Data.class.getName());
+    public static final DataFlavor RELATION_MEMBER_DATA = new DataFlavor(Data.class, "Relation member");
     private final Collection<RelationMember> members;
 
     /**
@@ -51,17 +69,17 @@ public class RelationMemberTransferable implements Transferable {
      * @param members list of relation members
      */
     public RelationMemberTransferable(Collection<RelationMember> members) {
-        this.members = members;
+        this.members = new ArrayList<>(members);
     }
 
     @Override
     public DataFlavor[] getTransferDataFlavors() {
-        return new DataFlavor[]{RELATION_MEMBER_DATA, DataFlavor.stringFlavor};
+        return new DataFlavor[]{RELATION_MEMBER_DATA, PrimitiveTransferData.DATA_FLAVOR, DataFlavor.stringFlavor};
     }
 
     @Override
     public boolean isDataFlavorSupported(DataFlavor flavor) {
-        return flavor == RELATION_MEMBER_DATA;
+        return Arrays.asList(getTransferDataFlavors()).contains(flavor);
     }
 
     @Override
@@ -70,10 +88,20 @@ public class RelationMemberTransferable implements Transferable {
             return getStringData();
         } else if (RELATION_MEMBER_DATA.equals(flavor)) {
             return getRelationMemberData();
+        } else if (PrimitiveTransferData.DATA_FLAVOR.equals(flavor)) {
+            return getPrimitiveData();
         }
         throw new UnsupportedFlavorException(flavor);
     }
 
+    private PrimitiveTransferData getPrimitiveData() {
+        Collection<OsmPrimitive> primitives = new HashSet<>();
+        for (RelationMember member : members) {
+            primitives.add(member.getMember());
+        }
+        return PrimitiveTransferData.getData(primitives);
+    }
+
     protected String getStringData() {
         final StringBuilder sb = new StringBuilder();
         for (RelationMember member : members) {
@@ -87,10 +115,6 @@ public class RelationMemberTransferable implements Transferable {
     }
 
     protected Data getRelationMemberData() {
-        final Collection<RelationMemberData> r = new ArrayList<>(members.size());
-        for (RelationMember member : members) {
-            r.add(new RelationMemberData(member.getRole(), member.getType(), member.getUniqueId()));
-        }
-        return new Data(r);
+        return Data.getData(members);
     }
 }
diff --git a/src/org/openstreetmap/josm/gui/datatransfer/data/PrimitiveTransferData.java b/src/org/openstreetmap/josm/gui/datatransfer/data/PrimitiveTransferData.java
new file mode 100644
index 0000000..ff68f09
--- /dev/null
+++ b/src/org/openstreetmap/josm/gui/datatransfer/data/PrimitiveTransferData.java
@@ -0,0 +1,165 @@
+// License: GPL. For details, see LICENSE file.
+package org.openstreetmap.josm.gui.datatransfer.data;
+
+import java.awt.datatransfer.DataFlavor;
+import java.io.Serializable;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.HashSet;
+import java.util.LinkedList;
+import java.util.Queue;
+
+import org.openstreetmap.josm.data.ProjectionBounds;
+import org.openstreetmap.josm.data.coor.EastNorth;
+import org.openstreetmap.josm.data.osm.NodeData;
+import org.openstreetmap.josm.data.osm.OsmPrimitive;
+import org.openstreetmap.josm.data.osm.PrimitiveData;
+import org.openstreetmap.josm.data.osm.Relation;
+import org.openstreetmap.josm.data.osm.Way;
+import org.openstreetmap.josm.data.osm.visitor.BoundingXYVisitor;
+
+/**
+ * A list of primitives that are transfered. The list allows you to implicitly add primitives.
+ * It distingluishes between primitives that were directly added and implicitly added ones.
+ * @author Michael Zangl
+ * @since xxx
+ */
+public final class PrimitiveTransferData implements Serializable {
+    private static final long serialVersionUID = 1L;
+
+    /**
+     * The data flavor used to represent this class.
+     */
+    public static final DataFlavor DATA_FLAVOR = new DataFlavor(PrimitiveTransferData.class, "OSM Primitives");
+
+    private static final class GetReferences implements ReferenceGetter {
+        @Override
+        public Collection<? extends OsmPrimitive> getReferedPrimitives(OsmPrimitive primitive) {
+            if (primitive instanceof Way) {
+                return ((Way) primitive).getNodes();
+            } else if (primitive instanceof Relation) {
+                return ((Relation) primitive).getMemberPrimitives();
+            } else {
+                return Collections.emptyList();
+            }
+        }
+    }
+
+    // TODO: Java8: Replace by Function.
+    private interface ReferenceGetter {
+        Collection<? extends OsmPrimitive> getReferedPrimitives(OsmPrimitive primitive);
+    }
+
+    private final ArrayList<PrimitiveData> direct;
+    private final ArrayList<PrimitiveData> referenced;
+
+    /**
+     * Create the new transfer data.
+     * @param primitives The primitives to transfer
+     * @param referencedGetter A function that allows to get the primitives referenced by the primitives variable.
+     * It will be queried recursively.
+     */
+    private PrimitiveTransferData(Collection<? extends OsmPrimitive> primitives, ReferenceGetter referencedGetter) {
+        // convert to hash set first to remove dupplicates
+        HashSet<OsmPrimitive> visited = new HashSet<OsmPrimitive>(primitives);
+        this.direct = new ArrayList<>(visited.size());
+
+        this.referenced = new ArrayList<>();
+        Queue<OsmPrimitive> toCheck = new LinkedList<>();
+        for (OsmPrimitive p : visited) {
+            direct.add(p.save());
+            toCheck.addAll(referencedGetter.getReferedPrimitives(p));
+        }
+        while (!toCheck.isEmpty()) {
+            OsmPrimitive p = toCheck.poll();
+            if (visited.add(p)) {
+                referenced.add(p.save());
+                toCheck.addAll(referencedGetter.getReferedPrimitives(p));
+            }
+        }
+    }
+
+    /**
+     * Gets all primitives directly added.
+     * @return The primitives
+     */
+    public Collection<PrimitiveData> getDirectlyAdded() {
+        return Collections.unmodifiableList(direct);
+    }
+
+    /**
+     * Gets all primitives that were added because they were referenced.
+     * @return The primitives
+     */
+    public Collection<PrimitiveData> getReferenced() {
+        return Collections.unmodifiableList(referenced);
+    }
+
+    /**
+     * Gets a List of all primitives added to this set.
+     * @return That list.
+     */
+    public Collection<PrimitiveData> getAll() {
+        ArrayList<PrimitiveData> list = new ArrayList<>();
+        list.addAll(direct);
+        list.addAll(referenced);
+        return list;
+    }
+
+    /**
+     * Creates a new {@link PrimitiveTransferData} object that only contains the primitives.
+     * @param primitives The primitives to contain.
+     * @return That set.
+     */
+    public static PrimitiveTransferData getData(Collection<? extends OsmPrimitive> primitives) {
+        return new PrimitiveTransferData(primitives, new ReferenceGetter() {
+            @Override
+            public Collection<? extends OsmPrimitive> getReferedPrimitives(OsmPrimitive primitive) {
+                return Collections.emptyList();
+            }
+        });
+    }
+
+
+    /**
+     * Creates a new {@link PrimitiveTransferData} object that contains the primitives and all references.
+     * @param primitives The primitives to contain.
+     * @return That set.
+     */
+    public static PrimitiveTransferData getDataWithReferences(Collection<? extends OsmPrimitive> primitives) {
+        return new PrimitiveTransferData(primitives, new GetReferences());
+    }
+
+    /**
+     * Compute the center of all nodes.
+     * @return The center or null if this buffer has no location.
+     */
+    public EastNorth getCenter() {
+        BoundingXYVisitor visitor = new BoundingXYVisitor();
+        for (PrimitiveData pd : getAll()) {
+            if (pd instanceof NodeData && !pd.isIncomplete()) {
+                visitor.visit(((NodeData) pd).getEastNorth());
+            }
+        }
+        ProjectionBounds bounds = visitor.getBounds();
+        if (bounds == null) {
+            return null;
+        } else {
+            return bounds.getCenter();
+        }
+    }
+
+    /**
+     * Tests wheter this set contains any primitives that have invalid data.
+     * @return <code>true</code> if invalid data is contained in this set.
+     */
+    public boolean hasIncompleteData() {
+        for (PrimitiveData pd : getAll()) {
+            if (pd.isIncomplete()) {
+                return true;
+            }
+        }
+        return false;
+    }
+}
diff --git a/src/org/openstreetmap/josm/gui/datatransfer/data/TagTransferData.java b/src/org/openstreetmap/josm/gui/datatransfer/data/TagTransferData.java
new file mode 100644
index 0000000..531d595
--- /dev/null
+++ b/src/org/openstreetmap/josm/gui/datatransfer/data/TagTransferData.java
@@ -0,0 +1,48 @@
+// License: GPL. For details, see LICENSE file.
+package org.openstreetmap.josm.gui.datatransfer.data;
+
+import java.awt.datatransfer.DataFlavor;
+import java.io.Serializable;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.Map;
+
+import org.openstreetmap.josm.data.osm.TagMap;
+import org.openstreetmap.josm.data.osm.Tagged;
+
+/**
+ * This is a special transfer type that only transfers tag data.
+ * <p>
+ * It currently contains all tags contained in the selection that was copied.
+ * @author Michael Zangl
+ * @since xxx
+ */
+public class TagTransferData implements Serializable {
+
+    private static final long serialVersionUID = 1;
+
+    /**
+     * This is a data flavor added
+     */
+    public static final DataFlavor FLAVOR = new DataFlavor(TagTransferData.class, "OSM Tags");
+
+    private final TagMap tags = new TagMap();
+
+    /**
+     * Creates a new {@link TagTransferData} object for the given objects.
+     * @param tagged The tags to transfer.
+     */
+    public TagTransferData(Collection<? extends Tagged> tagged) {
+        for (Tagged t : tagged) {
+            tags.putAll(t.getKeys());
+        }
+    }
+
+    /**
+     * Gets all tags contained in this data.
+     * @return The tags.
+     */
+    public Map<String, String> getTags() {
+        return Collections.unmodifiableMap(tags);
+    }
+}
diff --git a/src/org/openstreetmap/josm/gui/datatransfer/importers/AbstractOsmDataPaster.java b/src/org/openstreetmap/josm/gui/datatransfer/importers/AbstractOsmDataPaster.java
new file mode 100644
index 0000000..d370a39
--- /dev/null
+++ b/src/org/openstreetmap/josm/gui/datatransfer/importers/AbstractOsmDataPaster.java
@@ -0,0 +1,80 @@
+// License: GPL. For details, see LICENSE file.
+package org.openstreetmap.josm.gui.datatransfer.importers;
+
+import java.awt.datatransfer.DataFlavor;
+import java.awt.datatransfer.UnsupportedFlavorException;
+import java.io.IOException;
+import java.util.Collection;
+
+import javax.swing.TransferHandler;
+import javax.swing.TransferHandler.TransferSupport;
+
+import org.openstreetmap.josm.data.coor.EastNorth;
+import org.openstreetmap.josm.data.osm.OsmPrimitive;
+import org.openstreetmap.josm.gui.layer.OsmDataLayer;
+
+/**
+ * This is an abstract class that helps implementing the transfer support required by swing.
+ * <p>
+ * It implements a mechanism to import a given data flavor into the current OSM data layer.
+ * @author Michael Zangl
+ * @since xxx
+ */
+public abstract class AbstractOsmDataPaster {
+    protected final DataFlavor df;
+
+    /**
+     * Create a new {@link AbstractOsmDataPaster}
+     * @param df The data flavor that this support supports.
+     */
+    protected AbstractOsmDataPaster(DataFlavor df) {
+        this.df = df;
+    }
+
+    /**
+     * Checks if this supports importing the given transfer support.
+     * @param support The support that should be supported.
+     * @return True if we support that transfer.
+     */
+    public boolean supports(TransferSupport support) {
+        return support.isDataFlavorSupported(df) && isCopy(support);
+    }
+
+    /**
+     * Checks if this supports any of the available flavors.
+     * @param available The flavors that should be supported
+     * @return True if any of them is supported.
+     */
+    public boolean supports(Collection<DataFlavor> available) {
+        return available.contains(df);
+    }
+
+    private static boolean isCopy(TransferSupport support) {
+        return !support.isDrop() || (TransferHandler.COPY & support.getSourceDropActions()) == TransferHandler.COPY;
+    }
+
+    /**
+     * Attempts to import the given transfer data.
+     * @param support The transfer support to import from.
+     * @param layer The layer to paste at.
+     * @param pasteAt The position to paste at.
+     * @return <code>true</code> if the import was successful.
+     * @throws UnsupportedFlavorException
+     * @throws IOException
+     */
+    public abstract boolean importData(TransferSupport support, OsmDataLayer layer, EastNorth pasteAt)
+            throws UnsupportedFlavorException, IOException;
+
+    /**
+     * Imports only if this import changes the tags only. Does nothing if more than tags would be changed.
+     * @param support The support
+     * @param selection The primitives to apply on.
+     * @return <code>true</code> if an import was done.
+     * @throws UnsupportedFlavorException
+     * @throws IOException
+     */
+    public boolean importTagsOn(TransferSupport support, Collection<? extends OsmPrimitive> selection)
+            throws UnsupportedFlavorException, IOException {
+        return false;
+    }
+}
diff --git a/src/org/openstreetmap/josm/gui/datatransfer/importers/AbstractTagPaster.java b/src/org/openstreetmap/josm/gui/datatransfer/importers/AbstractTagPaster.java
new file mode 100644
index 0000000..dfc4cc7
--- /dev/null
+++ b/src/org/openstreetmap/josm/gui/datatransfer/importers/AbstractTagPaster.java
@@ -0,0 +1,56 @@
+// License: GPL. For details, see LICENSE file.
+package org.openstreetmap.josm.gui.datatransfer.importers;
+
+import java.awt.datatransfer.DataFlavor;
+import java.awt.datatransfer.UnsupportedFlavorException;
+import java.io.IOException;
+import java.util.Collection;
+import java.util.Map;
+
+import javax.swing.TransferHandler.TransferSupport;
+
+import org.openstreetmap.josm.Main;
+import org.openstreetmap.josm.command.ChangePropertyCommand;
+import org.openstreetmap.josm.data.coor.EastNorth;
+import org.openstreetmap.josm.data.osm.OsmPrimitive;
+import org.openstreetmap.josm.gui.layer.OsmDataLayer;
+
+/**
+ * This transfer support allows us to transfer tags to the selected primitives
+ * @author Michael Zangl
+ * @since xxx
+ */
+public abstract class AbstractTagPaster extends AbstractOsmDataPaster {
+
+    AbstractTagPaster(DataFlavor df) {
+        super(df);
+    }
+
+    @Override
+    public boolean importData(TransferSupport support, OsmDataLayer layer, EastNorth pasteAt)
+            throws UnsupportedFlavorException, IOException {
+        Collection<OsmPrimitive> selection = layer.data.getSelected();
+        if (selection.isEmpty()) {
+            return false;
+        }
+
+        return importTagsOn(support, selection);
+    }
+
+    @Override
+    public boolean importTagsOn(TransferSupport support, Collection<? extends OsmPrimitive> selection)
+            throws UnsupportedFlavorException, IOException {
+        ChangePropertyCommand command = new ChangePropertyCommand(selection, getTags(support));
+        Main.main.undoRedo.add(command);
+        return true;
+    }
+
+    /**
+     * Gets the tags that should be pasted.
+     * @param support The TransferSupport to get the tags from.
+     * @return The tags
+     * @throws UnsupportedFlavorException
+     * @throws IOException
+     */
+    protected abstract Map<String, String> getTags(TransferSupport support) throws UnsupportedFlavorException, IOException;
+}
diff --git a/src/org/openstreetmap/josm/gui/datatransfer/importers/FilePaster.java b/src/org/openstreetmap/josm/gui/datatransfer/importers/FilePaster.java
new file mode 100644
index 0000000..80ee372
--- /dev/null
+++ b/src/org/openstreetmap/josm/gui/datatransfer/importers/FilePaster.java
@@ -0,0 +1,41 @@
+// License: GPL. For details, see LICENSE file.
+package org.openstreetmap.josm.gui.datatransfer.importers;
+
+import java.awt.datatransfer.DataFlavor;
+import java.awt.datatransfer.UnsupportedFlavorException;
+import java.io.File;
+import java.io.IOException;
+import java.util.List;
+
+import javax.swing.TransferHandler.TransferSupport;
+
+import org.openstreetmap.josm.Main;
+import org.openstreetmap.josm.actions.OpenFileAction;
+import org.openstreetmap.josm.data.coor.EastNorth;
+import org.openstreetmap.josm.gui.layer.OsmDataLayer;
+
+/**
+ * This transfer support allows us to import a file that is dropped / copied on to the map.
+ * @author Michael Zangl
+ * @since xxx
+ */
+public final class FilePaster extends AbstractOsmDataPaster {
+    /**
+     * Create a new {@link FilePaster}
+     */
+    public FilePaster() {
+        super(DataFlavor.javaFileListFlavor);
+    }
+
+    @Override
+    public boolean importData(TransferSupport support, OsmDataLayer layer, EastNorth pasteAt)
+            throws UnsupportedFlavorException, IOException {
+        @SuppressWarnings("unchecked")
+        List<File> files = (List<File>) support.getTransferable().getTransferData(df);
+        OpenFileAction.OpenFileTask task = new OpenFileAction.OpenFileTask(files, null);
+        task.setRecordHistory(true);
+        Main.worker.submit(task);
+        return true;
+    }
+
+}
diff --git a/src/org/openstreetmap/josm/gui/datatransfer/importers/PrimitiveDataPaster.java b/src/org/openstreetmap/josm/gui/datatransfer/importers/PrimitiveDataPaster.java
new file mode 100644
index 0000000..5209c68
--- /dev/null
+++ b/src/org/openstreetmap/josm/gui/datatransfer/importers/PrimitiveDataPaster.java
@@ -0,0 +1,140 @@
+// License: GPL. For details, see LICENSE file.
+package org.openstreetmap.josm.gui.datatransfer.importers;
+
+import static org.openstreetmap.josm.tools.I18n.tr;
+
+import java.awt.datatransfer.UnsupportedFlavorException;
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.EnumMap;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+import javax.swing.TransferHandler.TransferSupport;
+
+import org.openstreetmap.josm.Main;
+import org.openstreetmap.josm.command.AddPrimitivesCommand;
+import org.openstreetmap.josm.data.coor.EastNorth;
+import org.openstreetmap.josm.data.osm.NodeData;
+import org.openstreetmap.josm.data.osm.OsmPrimitiveType;
+import org.openstreetmap.josm.data.osm.PrimitiveData;
+import org.openstreetmap.josm.data.osm.RelationData;
+import org.openstreetmap.josm.data.osm.RelationMemberData;
+import org.openstreetmap.josm.data.osm.WayData;
+import org.openstreetmap.josm.gui.ExtendedDialog;
+import org.openstreetmap.josm.gui.datatransfer.data.PrimitiveTransferData;
+import org.openstreetmap.josm.gui.layer.OsmDataLayer;
+
+/**
+ * This transfer support allows us to transfer primitives. This is the default paste action when primitives were copied.
+ * @author Michael Zangl
+ * @since xxx
+ */
+public final class PrimitiveDataPaster extends AbstractOsmDataPaster {
+    /**
+     * Create a new {@link PrimitiveDataPaster}
+     */
+    public PrimitiveDataPaster() {
+        super(PrimitiveTransferData.DATA_FLAVOR);
+    }
+
+    @Override
+    public boolean importData(TransferSupport support, final OsmDataLayer layer, EastNorth pasteAt)
+            throws UnsupportedFlavorException, IOException {
+        PrimitiveTransferData pasteBuffer = (PrimitiveTransferData) support.getTransferable().getTransferData(df);
+        // Allow to cancel paste if there are incomplete primitives
+        if (pasteBuffer.hasIncompleteData() && !confirmDeleteIncomplete()) {
+            return false;
+        }
+
+        EastNorth center = pasteBuffer.getCenter();
+        EastNorth offset = center == null ? null : pasteAt.subtract(center);
+
+        AddPrimitivesCommand command = createNewPrimitives(pasteBuffer, offset, layer);
+
+        /* Now execute the commands to add the duplicated contents of the paste buffer to the map */
+        Main.main.undoRedo.add(command);
+        return true;
+    }
+
+    private static AddPrimitivesCommand createNewPrimitives(PrimitiveTransferData pasteBuffer, EastNorth offset, OsmDataLayer layer) {
+        // Make a copy of pasteBuffer and map from old id to copied data id
+        List<PrimitiveData> bufferCopy = new ArrayList<>();
+        List<PrimitiveData> toSelect = new ArrayList<>();
+        EnumMap<OsmPrimitiveType, Map<Long, Long>> newIds = generateNewPrimitives(pasteBuffer, bufferCopy, toSelect);
+
+        // Update references in copied buffer
+        for (PrimitiveData data : bufferCopy) {
+            if (data instanceof NodeData) {
+                NodeData nodeData = (NodeData) data;
+                nodeData.setEastNorth(nodeData.getEastNorth().add(offset));
+            } else if (data instanceof WayData) {
+                updateNodes(newIds.get(OsmPrimitiveType.NODE), data);
+            } else if (data instanceof RelationData) {
+                updateMembers(newIds, data);
+            }
+        }
+        return new AddPrimitivesCommand(bufferCopy, toSelect, layer);
+    }
+
+    private static EnumMap<OsmPrimitiveType, Map<Long, Long>> generateNewPrimitives(PrimitiveTransferData pasteBuffer,
+            List<PrimitiveData> bufferCopy, List<PrimitiveData> toSelect) {
+        EnumMap<OsmPrimitiveType, Map<Long, Long>> newIds = new EnumMap<>(OsmPrimitiveType.class);
+        newIds.put(OsmPrimitiveType.NODE, new HashMap<Long, Long>());
+        newIds.put(OsmPrimitiveType.WAY, new HashMap<Long, Long>());
+        newIds.put(OsmPrimitiveType.RELATION, new HashMap<Long, Long>());
+
+        for (PrimitiveData data : pasteBuffer.getAll()) {
+            if (data.isIncomplete()) {
+                continue;
+            }
+            PrimitiveData copy = data.makeCopy();
+            // don't know why this is reset, but we need it to not crash on copying incomplete nodes.
+            boolean wasIncomplete = copy.isIncomplete();
+            copy.clearOsmMetadata();
+            copy.setIncomplete(wasIncomplete);
+            newIds.get(data.getType()).put(data.getUniqueId(), copy.getUniqueId());
+
+            bufferCopy.add(copy);
+            if (pasteBuffer.getDirectlyAdded().contains(data)) {
+                toSelect.add(copy);
+            }
+        }
+        return newIds;
+    }
+
+    private static void updateMembers(EnumMap<OsmPrimitiveType, Map<Long, Long>> newIds, PrimitiveData data) {
+        List<RelationMemberData> newMembers = new ArrayList<>();
+        for (RelationMemberData member : ((RelationData) data).getMembers()) {
+            OsmPrimitiveType memberType = member.getMemberType();
+            Long newId = newIds.get(memberType).get(member.getMemberId());
+            if (newId != null) {
+                newMembers.add(new RelationMemberData(member.getRole(), memberType, newId));
+            }
+        }
+        ((RelationData) data).setMembers(newMembers);
+    }
+
+    private static void updateNodes(Map<Long, Long> newNodeIds, PrimitiveData data) {
+        List<Long> newNodes = new ArrayList<>();
+        for (Long oldNodeId : ((WayData) data).getNodes()) {
+            Long newNodeId = newNodeIds.get(oldNodeId);
+            if (newNodeId != null) {
+                newNodes.add(newNodeId);
+            }
+        }
+        ((WayData) data).setNodes(newNodes);
+    }
+
+    private static boolean confirmDeleteIncomplete() {
+        ExtendedDialog ed = new ExtendedDialog(Main.parent, tr("Delete incomplete members?"),
+                new String[] { tr("Paste without incomplete members"), tr("Cancel") });
+        ed.setButtonIcons(new String[] { "dialogs/relation/deletemembers", "cancel" });
+        ed.setContent(tr(
+                "The copied data contains incomplete objects.  " + "When pasting the incomplete objects are removed.  "
+                        + "Do you want to paste the data without the incomplete objects?"));
+        ed.showDialog();
+        return ed.getValue() == 1;
+    }
+}
diff --git a/src/org/openstreetmap/josm/gui/datatransfer/importers/TagTransferPaster.java b/src/org/openstreetmap/josm/gui/datatransfer/importers/TagTransferPaster.java
new file mode 100644
index 0000000..23f9fd7
--- /dev/null
+++ b/src/org/openstreetmap/josm/gui/datatransfer/importers/TagTransferPaster.java
@@ -0,0 +1,30 @@
+// License: GPL. For details, see LICENSE file.
+package org.openstreetmap.josm.gui.datatransfer.importers;
+
+import java.awt.datatransfer.UnsupportedFlavorException;
+import java.io.IOException;
+import java.util.Map;
+
+import javax.swing.TransferHandler.TransferSupport;
+
+import org.openstreetmap.josm.gui.datatransfer.data.TagTransferData;
+
+/**
+ * This transfer support allows us to transfer tags from the copied primitives on to the selected ones.
+ * @author Michael Zangl
+ * @since xxx
+ */
+public final class TagTransferPaster extends AbstractTagPaster {
+    /**
+     * Create a new {@link TagTransferPaster}
+     */
+    public TagTransferPaster() {
+        super(TagTransferData.FLAVOR);
+    }
+
+    @Override
+    protected Map<String, String> getTags(TransferSupport support) throws UnsupportedFlavorException, IOException {
+        TagTransferData data = (TagTransferData) support.getTransferable().getTransferData(df);
+        return data.getTags();
+    }
+}
diff --git a/src/org/openstreetmap/josm/gui/datatransfer/importers/TextTagPaster.java b/src/org/openstreetmap/josm/gui/datatransfer/importers/TextTagPaster.java
new file mode 100644
index 0000000..cadc706
--- /dev/null
+++ b/src/org/openstreetmap/josm/gui/datatransfer/importers/TextTagPaster.java
@@ -0,0 +1,42 @@
+// License: GPL. For details, see LICENSE file.
+package org.openstreetmap.josm.gui.datatransfer.importers;
+
+import java.awt.datatransfer.DataFlavor;
+import java.awt.datatransfer.UnsupportedFlavorException;
+import java.io.IOException;
+import java.util.Map;
+
+import javax.swing.TransferHandler.TransferSupport;
+
+import org.openstreetmap.josm.Main;
+import org.openstreetmap.josm.tools.TextTagParser;
+
+/**
+ * This transfer support allows us to import tags from the text that was copied to the clippboard.
+ * @author Michael Zangl
+ * @since xxx
+ */
+public final class TextTagPaster extends AbstractTagPaster {
+
+    /**
+     * Create a new {@link TextTagPaster}
+     */
+    public TextTagPaster() {
+        super(DataFlavor.stringFlavor);
+    }
+
+    @Override
+    public boolean supports(TransferSupport support) {
+        try {
+            return super.supports(support) && getTags(support) != null;
+        } catch (UnsupportedFlavorException | IOException e) {
+            Main.warn(e);
+            return false;
+        }
+    }
+
+    @Override
+    protected Map<String, String> getTags(TransferSupport support) throws UnsupportedFlavorException, IOException {
+        return TextTagParser.readTagsFromText((String) support.getTransferable().getTransferData(df));
+    }
+}
diff --git a/src/org/openstreetmap/josm/gui/dialogs/OsmIdSelectionDialog.java b/src/org/openstreetmap/josm/gui/dialogs/OsmIdSelectionDialog.java
index ea87e40..da11869 100644
--- a/src/org/openstreetmap/josm/gui/dialogs/OsmIdSelectionDialog.java
+++ b/src/org/openstreetmap/josm/gui/dialogs/OsmIdSelectionDialog.java
@@ -33,6 +33,7 @@ import org.openstreetmap.josm.data.osm.OsmPrimitiveType;
 import org.openstreetmap.josm.data.osm.PrimitiveId;
 import org.openstreetmap.josm.data.osm.SimplePrimitiveId;
 import org.openstreetmap.josm.gui.ExtendedDialog;
+import org.openstreetmap.josm.gui.datatransfer.ClipboardUtils;
 import org.openstreetmap.josm.gui.widgets.HistoryComboBox;
 import org.openstreetmap.josm.gui.widgets.HtmlPanel;
 import org.openstreetmap.josm.gui.widgets.JosmTextField;
@@ -201,7 +202,7 @@ public class OsmIdSelectionDialog extends ExtendedDialog implements WindowListen
     }
 
     protected void tryToPasteFromClipboard(OsmIdTextField tfId, OsmPrimitiveTypesComboBox cbType) {
-        String buf = Utils.getClipboardContent();
+        String buf = ClipboardUtils.getClipboardStringContent();
         if (buf == null || buf.isEmpty()) return;
         if (buf.length() > Main.pref.getInteger("downloadprimitive.max-autopaste-length", 2000)) return;
         final List<SimplePrimitiveId> ids = SimplePrimitiveId.fuzzyParse(buf);
diff --git a/src/org/openstreetmap/josm/gui/dialogs/SelectionListDialog.java b/src/org/openstreetmap/josm/gui/dialogs/SelectionListDialog.java
index 3f56870..7935def 100644
--- a/src/org/openstreetmap/josm/gui/dialogs/SelectionListDialog.java
+++ b/src/org/openstreetmap/josm/gui/dialogs/SelectionListDialog.java
@@ -69,6 +69,7 @@ import org.openstreetmap.josm.gui.OsmPrimitivRenderer;
 import org.openstreetmap.josm.gui.PopupMenuHandler;
 import org.openstreetmap.josm.gui.SideButton;
 import org.openstreetmap.josm.gui.datatransfer.PrimitiveTransferable;
+import org.openstreetmap.josm.gui.datatransfer.data.PrimitiveTransferData;
 import org.openstreetmap.josm.gui.history.HistoryBrowserDialogManager;
 import org.openstreetmap.josm.gui.layer.MainLayerManager.ActiveLayerChangeEvent;
 import org.openstreetmap.josm.gui.layer.MainLayerManager.ActiveLayerChangeListener;
@@ -880,7 +881,7 @@ public class SelectionListDialog extends ToggleDialog {
 
         @Override
         protected Transferable createTransferable(JComponent c) {
-            return new PrimitiveTransferable(getSelectedPrimitives());
+            return new PrimitiveTransferable(PrimitiveTransferData.getData(getSelectedPrimitives()));
         }
     }
 }
diff --git a/src/org/openstreetmap/josm/gui/dialogs/properties/PropertiesDialog.java b/src/org/openstreetmap/josm/gui/dialogs/properties/PropertiesDialog.java
index 4b96ea6..4408f0a 100644
--- a/src/org/openstreetmap/josm/gui/dialogs/properties/PropertiesDialog.java
+++ b/src/org/openstreetmap/josm/gui/dialogs/properties/PropertiesDialog.java
@@ -86,6 +86,7 @@ import org.openstreetmap.josm.gui.DefaultNameFormatter;
 import org.openstreetmap.josm.gui.ExtendedDialog;
 import org.openstreetmap.josm.gui.PopupMenuHandler;
 import org.openstreetmap.josm.gui.SideButton;
+import org.openstreetmap.josm.gui.datatransfer.ClipboardUtils;
 import org.openstreetmap.josm.gui.dialogs.ToggleDialog;
 import org.openstreetmap.josm.gui.dialogs.relation.RelationEditor;
 import org.openstreetmap.josm.gui.help.HelpUtil;
@@ -1264,7 +1265,7 @@ implements SelectionChangedListener, ActiveLayerChangeListener, DataSetListenerA
                 return;
             String key = editHelper.getDataKey(tagTable.getSelectedRow());
             Collection<OsmPrimitive> sel = Main.main.getInProgressSelection();
-            String clipboard = Utils.getClipboardContent();
+            String clipboard = ClipboardUtils.getClipboardStringContent();
             if (sel.isEmpty() || clipboard == null)
                 return;
             Main.main.undoRedo.add(new ChangePropertyCommand(sel, key, Utils.strip(clipboard)));
@@ -1294,7 +1295,7 @@ implements SelectionChangedListener, ActiveLayerChangeListener, DataSetListenerA
                 }
             }
             if (!values.isEmpty()) {
-                Utils.copyToClipboard(Utils.join("\n", values));
+                ClipboardUtils.copyString(Utils.join("\n", values));
             }
         }
     }
diff --git a/src/org/openstreetmap/josm/gui/dialogs/properties/TagEditHelper.java b/src/org/openstreetmap/josm/gui/dialogs/properties/TagEditHelper.java
index b00d0ce..a4900a7 100644
--- a/src/org/openstreetmap/josm/gui/dialogs/properties/TagEditHelper.java
+++ b/src/org/openstreetmap/josm/gui/dialogs/properties/TagEditHelper.java
@@ -76,6 +76,7 @@ import org.openstreetmap.josm.data.preferences.EnumProperty;
 import org.openstreetmap.josm.data.preferences.IntegerProperty;
 import org.openstreetmap.josm.data.preferences.StringProperty;
 import org.openstreetmap.josm.gui.ExtendedDialog;
+import org.openstreetmap.josm.gui.datatransfer.ClipboardUtils;
 import org.openstreetmap.josm.gui.mappaint.MapPaintStyles;
 import org.openstreetmap.josm.gui.tagging.ac.AutoCompletingComboBox;
 import org.openstreetmap.josm.gui.tagging.ac.AutoCompletionListItem;
@@ -583,9 +584,9 @@ public class TagEditHelper {
 
         private void selectACComboBoxSavingUnixBuffer(AutoCompletingComboBox cb) {
             // select combobox with saving unix system selection (middle mouse paste)
-            Clipboard sysSel = GuiHelper.getSystemSelection();
+            Clipboard sysSel = ClipboardUtils.getSystemSelection();
             if (sysSel != null) {
-                Transferable old = Utils.getTransferableContent(sysSel);
+                Transferable old = ClipboardUtils.getClipboardContent(sysSel);
                 cb.requestFocusInWindow();
                 cb.getEditor().selectAll();
                 if (old != null) {
diff --git a/src/org/openstreetmap/josm/gui/dialogs/relation/GenericRelationEditor.java b/src/org/openstreetmap/josm/gui/dialogs/relation/GenericRelationEditor.java
index 47b563a..fd0175d 100644
--- a/src/org/openstreetmap/josm/gui/dialogs/relation/GenericRelationEditor.java
+++ b/src/org/openstreetmap/josm/gui/dialogs/relation/GenericRelationEditor.java
@@ -11,6 +11,8 @@ import java.awt.GraphicsEnvironment;
 import java.awt.GridBagConstraints;
 import java.awt.GridBagLayout;
 import java.awt.Window;
+import java.awt.datatransfer.Clipboard;
+import java.awt.datatransfer.FlavorListener;
 import java.awt.event.ActionEvent;
 import java.awt.event.FocusAdapter;
 import java.awt.event.FocusEvent;
@@ -61,6 +63,7 @@ import org.openstreetmap.josm.data.osm.Tag;
 import org.openstreetmap.josm.gui.ConditionalOptionPaneUtil;
 import org.openstreetmap.josm.gui.DefaultNameFormatter;
 import org.openstreetmap.josm.gui.MainMenu;
+import org.openstreetmap.josm.gui.datatransfer.ClipboardUtils;
 import org.openstreetmap.josm.gui.dialogs.relation.actions.AddSelectedAfterSelection;
 import org.openstreetmap.josm.gui.dialogs.relation.actions.AddSelectedAtEndAction;
 import org.openstreetmap.josm.gui.dialogs.relation.actions.AddSelectedAtStartAction;
@@ -152,6 +155,10 @@ public class GenericRelationEditor extends RelationEditor {
      * Action for performing the {@link CancelAction}
      */
     private final CancelAction cancelAction;
+    /**
+     * A list of listeners that need to be notified on clipboard content changes.
+     */
+    private final ArrayList<FlavorListener> clipboardListeners = new ArrayList<>();
 
     /**
      * Creates a new relation editor for the given relation. The relation will be saved if the user
@@ -273,7 +280,7 @@ public class GenericRelationEditor extends RelationEditor {
                 getRootPane(), memberTable, selectionTable);
         // CHECKSTYLE.ON: LineLength
 
-        registerCopyPasteAction(new PasteMembersAction(memberTableModel, getLayer(), this) {
+        registerCopyPasteAction(new PasteMembersAction(memberTable, getLayer(), this) {
             @Override
             public void actionPerformed(ActionEvent e) {
                 super.actionPerformed(e);
@@ -742,6 +749,7 @@ public class GenericRelationEditor extends RelationEditor {
             tagEditorPanel.initAutoCompletion(getLayer());
         }
         super.setVisible(visible);
+        Clipboard clipboard = ClipboardUtils.getClipboard();
         if (visible) {
             leftButtonToolbar.sortBelowButton.setVisible(ExpertToggleAction.isExpert());
             RelationDialogManager.getRelationDialogManager().positionOnScreen(this);
@@ -749,6 +757,10 @@ public class GenericRelationEditor extends RelationEditor {
                 windowMenuItem = addToWindowMenu(this, getLayer().getName());
             }
             tagEditorPanel.requestFocusInWindow();
+
+            for (FlavorListener listener : clipboardListeners) {
+                clipboard.addFlavorListener(listener);
+            }
         } else {
             // make sure all registered listeners are unregistered
             //
@@ -760,6 +772,9 @@ public class GenericRelationEditor extends RelationEditor {
                 Main.main.menu.windowMenu.remove(windowMenuItem);
                 windowMenuItem = null;
             }
+            for (FlavorListener listener : clipboardListeners) {
+                clipboard.removeFlavorListener(listener);
+            }
             dispose();
         }
     }
@@ -823,7 +838,7 @@ public class GenericRelationEditor extends RelationEditor {
         }
     }
 
-    private static void registerCopyPasteAction(AbstractAction action, Object actionName, KeyStroke shortcut,
+    private void registerCopyPasteAction(AbstractAction action, Object actionName, KeyStroke shortcut,
             JRootPane rootPane, JTable... tables) {
         int mods = shortcut.getModifiers();
         int code = shortcut.getKeyCode();
@@ -840,6 +855,9 @@ public class GenericRelationEditor extends RelationEditor {
             table.getInputMap(JComponent.WHEN_ANCESTOR_OF_FOCUSED_COMPONENT).put(shortcut, actionName);
             table.getInputMap(JComponent.WHEN_IN_FOCUSED_WINDOW).put(shortcut, actionName);
         }
+        if (action instanceof FlavorListener) {
+            clipboardListeners.add((FlavorListener) action);
+        }
     }
 
     /**
diff --git a/src/org/openstreetmap/josm/gui/dialogs/relation/MemberTable.java b/src/org/openstreetmap/josm/gui/dialogs/relation/MemberTable.java
index 324d3ff..f46eb66 100644
--- a/src/org/openstreetmap/josm/gui/dialogs/relation/MemberTable.java
+++ b/src/org/openstreetmap/josm/gui/dialogs/relation/MemberTable.java
@@ -85,6 +85,7 @@ public class MemberTable extends OsmPrimitivesTable implements IMemberModelListe
         JPopupMenu menu = super.buildPopupMenu();
         zoomToGap = new ZoomToGapAction();
         registerListeners();
+        menu.addSeparator();
         getSelectionModel().addListSelectionListener(zoomToGap);
         menu.add(zoomToGap);
         menu.addSeparator();
diff --git a/src/org/openstreetmap/josm/gui/dialogs/relation/MemberTableModel.java b/src/org/openstreetmap/josm/gui/dialogs/relation/MemberTableModel.java
index d72479f..3a84cd0 100644
--- a/src/org/openstreetmap/josm/gui/dialogs/relation/MemberTableModel.java
+++ b/src/org/openstreetmap/josm/gui/dialogs/relation/MemberTableModel.java
@@ -479,9 +479,7 @@ implements TableModelListener, SelectionChangedListener, DataSetListener, OsmPri
             members.add(idx++, member);
         }
         invalidateConnectionType();
-        final List<Integer> selection = getSelectedIndices();
         fireTableRowsInserted(index, idx - 1);
-        setSelectedMembersIdx(selection);
     }
 
     public void addMembersAtBeginning(List<? extends OsmPrimitive> primitives) {
diff --git a/src/org/openstreetmap/josm/gui/dialogs/relation/MemberTransferHandler.java b/src/org/openstreetmap/josm/gui/dialogs/relation/MemberTransferHandler.java
index 99fef5f..7f70cd0 100644
--- a/src/org/openstreetmap/josm/gui/dialogs/relation/MemberTransferHandler.java
+++ b/src/org/openstreetmap/josm/gui/dialogs/relation/MemberTransferHandler.java
@@ -16,13 +16,18 @@ import javax.swing.TransferHandler;
 import org.openstreetmap.josm.Main;
 import org.openstreetmap.josm.data.osm.OsmPrimitive;
 import org.openstreetmap.josm.data.osm.PrimitiveData;
+import org.openstreetmap.josm.data.osm.PrimitiveId;
 import org.openstreetmap.josm.data.osm.RelationMember;
 import org.openstreetmap.josm.data.osm.RelationMemberData;
-import org.openstreetmap.josm.gui.datatransfer.PrimitiveTransferable;
 import org.openstreetmap.josm.gui.datatransfer.RelationMemberTransferable;
-import org.openstreetmap.josm.tools.Utils.Function;
+import org.openstreetmap.josm.gui.datatransfer.data.PrimitiveTransferData;
 
-class MemberTransferHandler extends TransferHandler {
+/**
+ * A transfer handler that helps with importing / exporting members for relations.
+ * @author Michael Zangl
+ * @since xxx
+ */
+public class MemberTransferHandler extends TransferHandler {
 
     @Override
     public int getSourceActions(JComponent c) {
@@ -37,71 +42,84 @@ class MemberTransferHandler extends TransferHandler {
 
     @Override
     public boolean canImport(TransferSupport support) {
-        support.setShowDropLocation(true);
+        if (support.isDrop()) {
+            support.setShowDropLocation(true);
+        }
         return support.isDataFlavorSupported(RelationMemberTransferable.RELATION_MEMBER_DATA)
-                || support.isDataFlavorSupported(PrimitiveTransferable.PRIMITIVE_DATA);
+                || support.isDataFlavorSupported(PrimitiveTransferData.DATA_FLAVOR);
     }
 
     @Override
     public boolean importData(TransferSupport support) {
-        final MemberTable destination = (MemberTable) support.getComponent();
-        final int insertRow = ((JTable.DropLocation) support.getDropLocation()).getRow();
+        MemberTable destination = (MemberTable) support.getComponent();
+        int insertRow = computeInsertionRow(support, destination);
+
+        return importDataAt(support, destination, insertRow);
+
+    }
+
+    private int computeInsertionRow(TransferSupport support, MemberTable destination) {
+        final int insertRow;
+        if (support.isDrop()) {
+            insertRow = ((JTable.DropLocation) support.getDropLocation()).getRow();
+        } else {
+            int selection = destination.getSelectedRow();
+            if (selection < 0) {
+                // no selection, add at the end.
+                insertRow = destination.getRowCount();
+            } else {
+                insertRow = selection;
+            }
+        }
+        return insertRow;
+    }
 
+    private boolean importDataAt(TransferSupport support, MemberTable destination, int insertRow) {
         try {
             if (support.isDataFlavorSupported(RelationMemberTransferable.RELATION_MEMBER_DATA)) {
                 importRelationMemberData(support, destination, insertRow);
-            } else if (support.isDataFlavorSupported(PrimitiveTransferable.PRIMITIVE_DATA)) {
+                return true;
+            } else if (support.isDataFlavorSupported(PrimitiveTransferData.DATA_FLAVOR)) {
                 importPrimitiveData(support, destination, insertRow);
+                return true;
+            } else {
+                return false;
             }
         } catch (IOException | UnsupportedFlavorException e) {
             Main.warn(e);
             return false;
         }
-
-        return true;
     }
 
     protected void importRelationMemberData(TransferSupport support, final MemberTable destination, int insertRow)
             throws UnsupportedFlavorException, IOException {
         final RelationMemberTransferable.Data memberData = (RelationMemberTransferable.Data)
                 support.getTransferable().getTransferData(RelationMemberTransferable.RELATION_MEMBER_DATA);
-        importData(destination, insertRow, memberData.getRelationMemberData(), new Function<RelationMemberData, RelationMember>() {
+        importData(destination, insertRow, memberData.getRelationMemberData(), new AbstractRelationMemberConverter<RelationMemberData>() {
             @Override
-            public RelationMember apply(RelationMemberData member) {
-                final OsmPrimitive p = destination.getLayer().data.getPrimitiveById(member.getUniqueId(), member.getType());
-                if (p == null) {
-                    Main.warn(tr("Cannot add {0} since it is not part of dataset", member));
-                    return null;
-                } else {
-                    return new RelationMember(member.getRole(), p);
-                }
+            protected RelationMember getMember(MemberTable destination, RelationMemberData data, OsmPrimitive p) {
+                return new RelationMember(data.getRole(), p);
             }
         });
     }
 
     protected void importPrimitiveData(TransferSupport support, final MemberTable destination, int insertRow)
             throws UnsupportedFlavorException, IOException {
-        final PrimitiveTransferable.Data data = (PrimitiveTransferable.Data)
-                support.getTransferable().getTransferData(PrimitiveTransferable.PRIMITIVE_DATA);
-        importData(destination, insertRow, data.getPrimitiveData(), new Function<PrimitiveData, RelationMember>() {
+        final PrimitiveTransferData data = (PrimitiveTransferData)
+                support.getTransferable().getTransferData(PrimitiveTransferData.DATA_FLAVOR);
+        importData(destination, insertRow, data.getDirectlyAdded(), new AbstractRelationMemberConverter<PrimitiveData>() {
             @Override
-            public RelationMember apply(PrimitiveData data) {
-                final OsmPrimitive p = destination.getLayer().data.getPrimitiveById(data);
-                if (p == null) {
-                    Main.warn(tr("Cannot add {0} since it is not part of dataset", data));
-                    return null;
-                } else {
-                    return destination.getMemberTableModel().getRelationMemberForPrimitive(p);
-                }
+            protected RelationMember getMember(MemberTable destination, PrimitiveData data, OsmPrimitive p) {
+                return destination.getMemberTableModel().getRelationMemberForPrimitive(p);
             }
         });
     }
 
-    protected <T> void importData(MemberTable destination, int insertRow,
-                                  Collection<T> memberData, Function<T, RelationMember> toMemberFunction) {
+    protected <T extends PrimitiveId> void importData(MemberTable destination, int insertRow,
+                                  Collection<T> memberData, AbstractRelationMemberConverter<T> toMemberFunction) {
         final Collection<RelationMember> membersToAdd = new ArrayList<>(memberData.size());
-        for (T i : memberData) {
-            final RelationMember member = toMemberFunction.apply(i);
+        for (T data : memberData) {
+            final RelationMember member = toMemberFunction.importPrimitive(destination, data);
             if (member != null) {
                 membersToAdd.add(member);
             }
@@ -119,4 +137,18 @@ class MemberTransferHandler extends TransferHandler {
         model.remove(source.getSelectedRows());
         model.selectionChanged(null);
     }
+
+    private abstract static class AbstractRelationMemberConverter<T extends PrimitiveId> {
+        protected RelationMember importPrimitive(MemberTable destination, T data) {
+            final OsmPrimitive p = destination.getLayer().data.getPrimitiveById(data);
+            if (p == null) {
+                Main.warn(tr("Cannot add {0} since it is not part of dataset", data));
+                return null;
+            } else {
+                return getMember(destination, data, p);
+            }
+        }
+
+        protected abstract RelationMember getMember(MemberTable destination, T data, OsmPrimitive p);
+    }
 }
diff --git a/src/org/openstreetmap/josm/gui/dialogs/relation/actions/CopyMembersAction.java b/src/org/openstreetmap/josm/gui/dialogs/relation/actions/CopyMembersAction.java
index 952e554..8b8ba36 100644
--- a/src/org/openstreetmap/josm/gui/dialogs/relation/actions/CopyMembersAction.java
+++ b/src/org/openstreetmap/josm/gui/dialogs/relation/actions/CopyMembersAction.java
@@ -4,8 +4,9 @@ package org.openstreetmap.josm.gui.dialogs.relation.actions;
 import java.awt.event.ActionEvent;
 import java.util.Collection;
 
-import org.openstreetmap.josm.actions.CopyAction;
-import org.openstreetmap.josm.data.osm.OsmPrimitive;
+import org.openstreetmap.josm.data.osm.RelationMember;
+import org.openstreetmap.josm.gui.datatransfer.ClipboardUtils;
+import org.openstreetmap.josm.gui.datatransfer.RelationMemberTransferable;
 import org.openstreetmap.josm.gui.dialogs.relation.IRelationEditor;
 import org.openstreetmap.josm.gui.dialogs.relation.MemberTableModel;
 import org.openstreetmap.josm.gui.layer.OsmDataLayer;
@@ -28,9 +29,10 @@ public class CopyMembersAction extends AddFromSelectionAction {
 
     @Override
     public void actionPerformed(ActionEvent e) {
-        final Collection<OsmPrimitive> primitives = memberTableModel.getSelectedChildPrimitives();
-        if (!primitives.isEmpty()) {
-            CopyAction.copy(layer, primitives);
+        final Collection<RelationMember> members = memberTableModel.getSelectedMembers();
+
+        if (!members.isEmpty()) {
+            ClipboardUtils.copy(new RelationMemberTransferable(members));
         }
     }
 
diff --git a/src/org/openstreetmap/josm/gui/dialogs/relation/actions/PasteMembersAction.java b/src/org/openstreetmap/josm/gui/dialogs/relation/actions/PasteMembersAction.java
index eaa08d2..2cf4c16 100644
--- a/src/org/openstreetmap/josm/gui/dialogs/relation/actions/PasteMembersAction.java
+++ b/src/org/openstreetmap/josm/gui/dialogs/relation/actions/PasteMembersAction.java
@@ -1,81 +1,51 @@
 // License: GPL. For details, see LICENSE file.
 package org.openstreetmap.josm.gui.dialogs.relation.actions;
 
-import static org.openstreetmap.josm.tools.I18n.tr;
-
+import java.awt.datatransfer.FlavorEvent;
+import java.awt.datatransfer.FlavorListener;
 import java.awt.event.ActionEvent;
-import java.util.ArrayList;
-import java.util.List;
 
-import javax.swing.JOptionPane;
+import javax.swing.TransferHandler.TransferSupport;
 
-import org.openstreetmap.josm.Main;
-import org.openstreetmap.josm.data.osm.DataSet;
-import org.openstreetmap.josm.data.osm.OsmPrimitive;
-import org.openstreetmap.josm.data.osm.PrimitiveData;
-import org.openstreetmap.josm.gui.dialogs.relation.GenericRelationEditor.AddAbortException;
+import org.openstreetmap.josm.gui.datatransfer.ClipboardUtils;
 import org.openstreetmap.josm.gui.dialogs.relation.IRelationEditor;
-import org.openstreetmap.josm.gui.dialogs.relation.MemberTableModel;
+import org.openstreetmap.josm.gui.dialogs.relation.MemberTable;
+import org.openstreetmap.josm.gui.dialogs.relation.MemberTransferHandler;
 import org.openstreetmap.josm.gui.layer.OsmDataLayer;
 
 /**
  * Paste members.
  * @since 9496
  */
-public class PasteMembersAction extends AddFromSelectionAction {
+public class PasteMembersAction extends AddFromSelectionAction implements FlavorListener {
 
     /**
      * Constructs a new {@code PasteMembersAction}.
-     * @param memberTableModel member table model
+     * @param memberTable member table
      * @param layer OSM data layer
      * @param editor relation editor
      */
-    public PasteMembersAction(MemberTableModel memberTableModel, OsmDataLayer layer, IRelationEditor editor) {
-        super(null, memberTableModel, null, null, null, layer, editor);
+    public PasteMembersAction(MemberTable memberTable, OsmDataLayer layer, IRelationEditor editor) {
+        super(memberTable, null, null, null, null, layer, editor);
+        updateEnabledState();
     }
 
     @Override
     public void actionPerformed(ActionEvent e) {
-        try {
-            List<PrimitiveData> primitives = Main.pasteBuffer.getDirectlyAdded();
-            DataSet ds = layer.data;
-            List<OsmPrimitive> toAdd = new ArrayList<>();
-            boolean hasNewInOtherLayer = false;
-
-            for (PrimitiveData primitive: primitives) {
-                OsmPrimitive primitiveInDs = ds.getPrimitiveById(primitive);
-                if (primitiveInDs != null) {
-                    toAdd.add(primitiveInDs);
-                } else if (!primitive.isNew()) {
-                    OsmPrimitive p = primitive.getType().newInstance(primitive.getUniqueId(), true);
-                    ds.addPrimitive(p);
-                    toAdd.add(p);
-                } else {
-                    hasNewInOtherLayer = true;
-                    break;
-                }
-            }
-
-            if (hasNewInOtherLayer) {
-                JOptionPane.showMessageDialog(Main.parent,
-                        tr("Members from paste buffer cannot be added because they are not included in current layer"));
-                return;
-            }
-
-            toAdd = filterConfirmedPrimitives(toAdd);
-            int index = memberTableModel.getSelectionModel().getMaxSelectionIndex();
-            if (index == -1) {
-                index = memberTableModel.getRowCount() - 1;
-            }
-            memberTableModel.addMembersAfterIdx(toAdd, index);
+        new MemberTransferHandler().importData(getSupport());
+    }
 
-        } catch (AddAbortException ex) {
-            Main.trace(ex);
-        }
+    private TransferSupport getSupport() {
+        return new TransferSupport(memberTable, ClipboardUtils.getClipboard().getContents(null));
     }
 
     @Override
     protected void updateEnabledState() {
-        // Do nothing
+        setEnabled(new MemberTransferHandler().canImport(getSupport()));
+    }
+
+    @Override
+    public void flavorsChanged(FlavorEvent e) {
+        updateEnabledState();
     }
 }
diff --git a/src/org/openstreetmap/josm/gui/download/DownloadDialog.java b/src/org/openstreetmap/josm/gui/download/DownloadDialog.java
index c937092..b444bbd 100644
--- a/src/org/openstreetmap/josm/gui/download/DownloadDialog.java
+++ b/src/org/openstreetmap/josm/gui/download/DownloadDialog.java
@@ -38,6 +38,7 @@ import org.openstreetmap.josm.Main;
 import org.openstreetmap.josm.actions.ExpertToggleAction;
 import org.openstreetmap.josm.data.Bounds;
 import org.openstreetmap.josm.gui.MapView;
+import org.openstreetmap.josm.gui.datatransfer.ClipboardUtils;
 import org.openstreetmap.josm.gui.help.ContextSensitiveHelpAction;
 import org.openstreetmap.josm.gui.help.HelpUtil;
 import org.openstreetmap.josm.gui.util.GuiHelper;
@@ -242,7 +243,7 @@ public class DownloadDialog extends JDialog {
         getRootPane().getActionMap().put("checkClipboardContents", new AbstractAction() {
             @Override
             public void actionPerformed(ActionEvent e) {
-                String clip = Utils.getClipboardContent();
+                String clip = ClipboardUtils.getClipboardStringContent();
                 if (clip == null) {
                     return;
                 }
diff --git a/src/org/openstreetmap/josm/gui/layer/NoteLayer.java b/src/org/openstreetmap/josm/gui/layer/NoteLayer.java
index 90f65b0..4b9c1a8 100644
--- a/src/org/openstreetmap/josm/gui/layer/NoteLayer.java
+++ b/src/org/openstreetmap/josm/gui/layer/NoteLayer.java
@@ -30,6 +30,7 @@ import org.openstreetmap.josm.data.notes.NoteComment;
 import org.openstreetmap.josm.data.osm.NoteData;
 import org.openstreetmap.josm.data.osm.visitor.BoundingXYVisitor;
 import org.openstreetmap.josm.gui.MapView;
+import org.openstreetmap.josm.gui.datatransfer.ClipboardUtils;
 import org.openstreetmap.josm.gui.dialogs.LayerListDialog;
 import org.openstreetmap.josm.gui.dialogs.LayerListPopup;
 import org.openstreetmap.josm.gui.io.AbstractIOTask;
@@ -40,7 +41,6 @@ import org.openstreetmap.josm.io.OsmApi;
 import org.openstreetmap.josm.io.XmlWriter;
 import org.openstreetmap.josm.tools.ColorHelper;
 import org.openstreetmap.josm.tools.ImageProvider;
-import org.openstreetmap.josm.tools.Utils;
 import org.openstreetmap.josm.tools.date.DateUtils;
 
 /**
@@ -237,7 +237,7 @@ public class NoteLayer extends AbstractModifiableLayer implements MouseListener
     public void mouseClicked(MouseEvent e) {
         if (SwingUtilities.isRightMouseButton(e) && noteData.getSelectedNote() != null) {
             final String url = OsmApi.getOsmApi().getBaseUrl() + "notes/" + noteData.getSelectedNote().getId();
-            Utils.copyToClipboard(url);
+            ClipboardUtils.copyString(url);
             return;
         } else if (!SwingUtilities.isLeftMouseButton(e)) {
             return;
diff --git a/src/org/openstreetmap/josm/gui/layer/geoimage/GeoImageLayer.java b/src/org/openstreetmap/josm/gui/layer/geoimage/GeoImageLayer.java
index 6c58bca..783b9b2 100644
--- a/src/org/openstreetmap/josm/gui/layer/geoimage/GeoImageLayer.java
+++ b/src/org/openstreetmap/josm/gui/layer/geoimage/GeoImageLayer.java
@@ -52,6 +52,7 @@ import org.openstreetmap.josm.gui.MapFrame.MapModeChangeListener;
 import org.openstreetmap.josm.gui.MapView;
 import org.openstreetmap.josm.gui.NavigatableComponent;
 import org.openstreetmap.josm.gui.PleaseWaitRunnable;
+import org.openstreetmap.josm.gui.datatransfer.ClipboardUtils;
 import org.openstreetmap.josm.gui.dialogs.LayerListDialog;
 import org.openstreetmap.josm.gui.dialogs.LayerListPopup;
 import org.openstreetmap.josm.gui.layer.AbstractModifiableLayer;
@@ -724,7 +725,7 @@ public class GeoImageLayer extends AbstractModifiableLayer implements PropertyCh
 
     public void copyCurrentPhotoPath() {
         if (data != null && !data.isEmpty() && currentPhoto >= 0 && currentPhoto < data.size()) {
-            Utils.copyToClipboard(data.get(currentPhoto).getFile().toString());
+            ClipboardUtils.copyString(data.get(currentPhoto).getFile().toString());
         }
     }
 
diff --git a/src/org/openstreetmap/josm/gui/tagging/TagEditorModel.java b/src/org/openstreetmap/josm/gui/tagging/TagEditorModel.java
index 53499d8..90e3efa 100644
--- a/src/org/openstreetmap/josm/gui/tagging/TagEditorModel.java
+++ b/src/org/openstreetmap/josm/gui/tagging/TagEditorModel.java
@@ -7,6 +7,7 @@ import java.beans.PropertyChangeListener;
 import java.beans.PropertyChangeSupport;
 import java.util.ArrayList;
 import java.util.Collection;
+import java.util.Collections;
 import java.util.Comparator;
 import java.util.EnumSet;
 import java.util.HashMap;
@@ -24,6 +25,7 @@ import org.openstreetmap.josm.command.SequenceCommand;
 import org.openstreetmap.josm.data.osm.OsmPrimitive;
 import org.openstreetmap.josm.data.osm.Tag;
 import org.openstreetmap.josm.data.osm.TagCollection;
+import org.openstreetmap.josm.data.osm.TagMap;
 import org.openstreetmap.josm.data.osm.Tagged;
 import org.openstreetmap.josm.gui.tagging.presets.TaggingPresetType;
 import org.openstreetmap.josm.tools.CheckParameterUtil;
@@ -47,6 +49,8 @@ public class TagEditorModel extends AbstractTableModel {
 
     private transient OsmPrimitive primitive;
 
+    private EndEditListener endEditListener;
+
     /**
      * Creates a new tag editor model. Internally allocates two selection models
      * for row selection and column selection.
@@ -166,6 +170,7 @@ public class TagEditorModel extends AbstractTableModel {
      * removes all tags in the model
      */
     public void clear() {
+        commitPendingEdit();
         boolean wasEmpty = tags.isEmpty();
         tags.clear();
         if (!wasEmpty) {
@@ -182,13 +187,24 @@ public class TagEditorModel extends AbstractTableModel {
      * @throws IllegalArgumentException if tag is null
      */
     public void add(TagModel tag) {
+        commitPendingEdit();
         CheckParameterUtil.ensureParameterNotNull(tag, "tag");
         tags.add(tag);
         setDirty(true);
         fireTableDataChanged();
     }
 
+    /**
+     * Add a tag at the beginning of the table.
+     *
+     * @param tag The tag to add
+     *
+     * @throws IllegalArgumentException if tag is null
+     *
+     * @see #add(TagModel)
+     */
     public void prepend(TagModel tag) {
+        commitPendingEdit();
         CheckParameterUtil.ensureParameterNotNull(tag, "tag");
         tags.add(0, tag);
         setDirty(true);
@@ -208,6 +224,7 @@ public class TagEditorModel extends AbstractTableModel {
      * @param value the value; converted to "" if null
      */
     public void add(String name, String value) {
+        commitPendingEdit();
         String key = (name == null) ? "" : name;
         String val = (value == null) ? "" : value;
 
@@ -258,6 +275,7 @@ public class TagEditorModel extends AbstractTableModel {
     public void deleteTagNames(int[] tagIndices) {
         if (tags == null)
             return;
+        commitPendingEdit();
         for (int tagIdx : tagIndices) {
             TagModel tag = tags.get(tagIdx);
             if (tag != null) {
@@ -276,6 +294,7 @@ public class TagEditorModel extends AbstractTableModel {
     public void deleteTagValues(int[] tagIndices) {
         if (tags == null)
             return;
+        commitPendingEdit();
         for (int tagIdx : tagIndices) {
             TagModel tag = tags.get(tagIdx);
             if (tag != null) {
@@ -292,6 +311,7 @@ public class TagEditorModel extends AbstractTableModel {
      * @param name the name. Ignored if null.
      */
     public void delete(String name) {
+        commitPendingEdit();
         if (name == null)
             return;
         Iterator<TagModel> it = tags.iterator();
@@ -317,6 +337,7 @@ public class TagEditorModel extends AbstractTableModel {
     public void deleteTags(int[] tagIndices) {
         if (tags == null)
             return;
+        commitPendingEdit();
         List<TagModel> toDelete = new ArrayList<>();
         for (int tagIdx : tagIndices) {
             TagModel tag = tags.get(tagIdx);
@@ -355,13 +376,14 @@ public class TagEditorModel extends AbstractTableModel {
      * @param primitive the OSM primitive
      */
     public void initFromPrimitive(Tagged primitive) {
+        commitPendingEdit();
         this.tags.clear();
         for (String key : primitive.keySet()) {
             String value = primitive.get(key);
             this.tags.add(new TagModel(key, value));
         }
-        TagModel tag = new TagModel();
         sort();
+        TagModel tag = new TagModel();
         tags.add(tag);
         setDirty(false);
         fireTableDataChanged();
@@ -373,6 +395,7 @@ public class TagEditorModel extends AbstractTableModel {
      * @param tags the tags of an OSM primitive
      */
     public void initFromTags(Map<String, String> tags) {
+        commitPendingEdit();
         this.tags.clear();
         for (Entry<String, String> entry : tags.entrySet()) {
             this.tags.add(new TagModel(entry.getKey(), entry.getValue()));
@@ -390,6 +413,7 @@ public class TagEditorModel extends AbstractTableModel {
      * @param tags the tags
      */
     public void initFromTags(TagCollection tags) {
+        commitPendingEdit();
         this.tags.clear();
         if (tags == null) {
             setDirty(false);
@@ -423,7 +447,8 @@ public class TagEditorModel extends AbstractTableModel {
      * @return the map of key/value pairs
      */
     private Map<String, String> applyToTags(boolean keepEmpty) {
-        Map<String, String> result = new HashMap<>();
+        // TagMap preserves the order of tags.
+        TagMap result = new TagMap();
         for (TagModel tag: this.tags) {
             // tag still holds an unchanged list of different values for the same key.
             // no property change command required
@@ -432,7 +457,6 @@ public class TagEditorModel extends AbstractTableModel {
             }
 
             // tag name holds an empty key. Don't apply it to the selection.
-            //
             if (!keepEmpty && (tag.getName().trim().isEmpty() || tag.getValue().trim().isEmpty())) {
                 continue;
             }
@@ -538,7 +562,7 @@ public class TagEditorModel extends AbstractTableModel {
      * sorts the current tags according alphabetical order of names
      */
     protected void sort() {
-        java.util.Collections.sort(
+        Collections.sort(
                 tags,
                 new Comparator<TagModel>() {
                     @Override
@@ -590,9 +614,10 @@ public class TagEditorModel extends AbstractTableModel {
      * @param tags - the list
      */
     public void updateTags(List<Tag> tags) {
-         if (tags.isEmpty())
+        if (tags.isEmpty())
             return;
 
+        commitPendingEdit();
         Map<String, TagModel> modelTags = new HashMap<>();
         for (int i = 0; i < getRowCount(); i++) {
             TagModel tagModel = get(i);
@@ -647,6 +672,20 @@ public class TagEditorModel extends AbstractTableModel {
         return this;
     }
 
+    /**
+     * Sets the listener that is notified when an edit should be aborted.
+     * @param endEditListener The listener to be notified when editing should be aborted.
+     */
+    public void setEndEditListener(EndEditListener endEditListener) {
+        this.endEditListener = endEditListener;
+    }
+
+    private void commitPendingEdit() {
+        if (endEditListener != null) {
+            endEditListener.endCellEditing();
+        }
+    }
+
     class SelectionStateMemento {
         private final int rowMin;
         private final int rowMax;
@@ -673,4 +712,14 @@ public class TagEditorModel extends AbstractTableModel {
             colSelectionModel.setValueIsAdjusting(false);
         }
     }
+
+    /**
+     * A listener that is called whenever the cells may be updated from outside the editor and the editor should thus be commited.
+     */
+    public interface EndEditListener {
+        /**
+         * Requests to end the editing of any cells on this model
+         */
+        public void endCellEditing();
+    }
 }
diff --git a/src/org/openstreetmap/josm/gui/tagging/TagTable.java b/src/org/openstreetmap/josm/gui/tagging/TagTable.java
index cea481e..6d2899e 100644
--- a/src/org/openstreetmap/josm/gui/tagging/TagTable.java
+++ b/src/org/openstreetmap/josm/gui/tagging/TagTable.java
@@ -1,7 +1,6 @@
 // License: GPL. For details, see LICENSE file.
 package org.openstreetmap.josm.gui.tagging;
 
-import static org.openstreetmap.josm.gui.help.HelpUtil.ht;
 import static org.openstreetmap.josm.tools.I18n.tr;
 
 import java.awt.Component;
@@ -12,11 +11,8 @@ import java.awt.event.ActionEvent;
 import java.awt.event.KeyEvent;
 import java.beans.PropertyChangeEvent;
 import java.beans.PropertyChangeListener;
-import java.util.ArrayList;
 import java.util.Collections;
 import java.util.EventObject;
-import java.util.List;
-import java.util.Map;
 import java.util.concurrent.CopyOnWriteArrayList;
 
 import javax.swing.AbstractAction;
@@ -31,24 +27,20 @@ import javax.swing.event.ListSelectionListener;
 import javax.swing.text.JTextComponent;
 
 import org.openstreetmap.josm.Main;
-import org.openstreetmap.josm.actions.CopyAction;
-import org.openstreetmap.josm.actions.PasteTagsAction;
-import org.openstreetmap.josm.data.osm.OsmPrimitive;
-import org.openstreetmap.josm.data.osm.PrimitiveData;
 import org.openstreetmap.josm.data.osm.Relation;
-import org.openstreetmap.josm.data.osm.Tag;
+import org.openstreetmap.josm.data.osm.TagMap;
+import org.openstreetmap.josm.gui.datatransfer.OsmTransferHandler;
+import org.openstreetmap.josm.gui.tagging.TagEditorModel.EndEditListener;
 import org.openstreetmap.josm.gui.tagging.ac.AutoCompletionList;
 import org.openstreetmap.josm.gui.tagging.ac.AutoCompletionManager;
 import org.openstreetmap.josm.gui.widgets.JosmTable;
 import org.openstreetmap.josm.tools.ImageProvider;
-import org.openstreetmap.josm.tools.TextTagParser;
-import org.openstreetmap.josm.tools.Utils;
 
 /**
  * This is the tabular editor component for OSM tags.
  * @since 1762
  */
-public class TagTable extends JosmTable {
+public class TagTable extends JosmTable implements EndEditListener {
     /** the table cell editor used by this table */
     private TagCellEditor editor;
     private final TagEditorModel model;
@@ -210,12 +202,7 @@ public class TagTable extends JosmTable {
             default: // Do nothing
             }
 
-            if (isEditing()) {
-                CellEditor cEditor = getCellEditor();
-                if (cEditor != null) {
-                    cEditor.cancelCellEditing();
-                }
-            }
+            endCellEditing();
 
             if (model.getRowCount() == 0) {
                 model.ensureOneTag();
@@ -232,11 +219,7 @@ public class TagTable extends JosmTable {
         }
 
         protected final void updateEnabledState() {
-            if (isEditing() && getSelectedColumnCount() == 1 && getSelectedRowCount() == 1) {
-                setEnabled(true);
-            } else if (!isEditing() && getSelectedColumnCount() == 1 && getSelectedRowCount() == 1) {
-                setEnabled(true);
-            } else if (getSelectedColumnCount() > 1 || getSelectedRowCount() > 1) {
+            if (getSelectedColumnCount() >= 1 && getSelectedRowCount() >= 1) {
                 setEnabled(true);
             } else {
                 setEnabled(false);
@@ -294,29 +277,8 @@ public class TagTable extends JosmTable {
         public void actionPerformed(ActionEvent e) {
             Relation relation = new Relation();
             model.applyToPrimitive(relation);
-
-            String buf = Utils.getClipboardContent();
-            if (buf == null || buf.isEmpty() || buf.matches(CopyAction.CLIPBOARD_REGEXP)) {
-                List<PrimitiveData> directlyAdded = Main.pasteBuffer.getDirectlyAdded();
-                if (directlyAdded == null || directlyAdded.isEmpty()) return;
-                PasteTagsAction.TagPaster tagPaster = new PasteTagsAction.TagPaster(directlyAdded,
-                        Collections.<OsmPrimitive>singletonList(relation));
-                model.updateTags(tagPaster.execute());
-            } else {
-                 // Paste tags from arbitrary text
-                 Map<String, String> tags = TextTagParser.readTagsFromText(buf);
-                 if (tags == null || tags.isEmpty()) {
-                    TextTagParser.showBadBufferMessage(ht("/Action/PasteTags"));
-                 } else if (TextTagParser.validateTags(tags)) {
-                     List<Tag> newTags = new ArrayList<>();
-                     for (Map.Entry<String, String> entry: tags.entrySet()) {
-                        String k = entry.getKey();
-                        String v = entry.getValue();
-                        newTags.add(new Tag(k, v));
-                     }
-                     model.updateTags(newTags);
-                 }
-            }
+            new OsmTransferHandler().pasteTags(Collections.singleton(relation));
+            model.updateTags(new TagMap(relation.getKeys()).getTags());
         }
 
         protected final void updateEnabledState() {
@@ -414,6 +376,7 @@ public class TagTable extends JosmTable {
                   .setSelectionModel(model.getColumnSelectionModel()).build(),
               model.getRowSelectionModel());
         this.model = model;
+        model.setEndEditListener(this);
         init(maxCharacters);
     }
 
@@ -487,9 +450,7 @@ public class TagTable extends JosmTable {
      * @param editor tag cell editor
      */
     public void setTagCellEditor(TagCellEditor editor) {
-        if (isEditing()) {
-            this.editor.cancelCellEditing();
-        }
+        endCellEditing();
         this.editor = editor;
         getColumnModel().getColumn(0).setCellEditor(editor);
         getColumnModel().getColumn(1).setCellEditor(editor);
@@ -548,6 +509,18 @@ public class TagTable extends JosmTable {
     }
 
     @Override
+    public void endCellEditing() {
+        if (isEditing()) {
+            CellEditor cEditor = getCellEditor();
+            if (cEditor != null) {
+                // First attempt to commit. If this does not work, cancel.
+                cEditor.stopCellEditing();
+                cEditor.cancelCellEditing();
+            }
+        }
+    }
+
+    @Override
     public void removeEditor() {
         // make sure we unregister our custom implementation of CellEditorRemover
         KeyboardFocusManager.getCurrentKeyboardFocusManager().
diff --git a/src/org/openstreetmap/josm/gui/tagging/ac/AutoCompletingComboBox.java b/src/org/openstreetmap/josm/gui/tagging/ac/AutoCompletingComboBox.java
index c46e863..c98a0df 100644
--- a/src/org/openstreetmap/josm/gui/tagging/ac/AutoCompletingComboBox.java
+++ b/src/org/openstreetmap/josm/gui/tagging/ac/AutoCompletingComboBox.java
@@ -23,9 +23,8 @@ import javax.swing.text.PlainDocument;
 import javax.swing.text.StyleConstants;
 
 import org.openstreetmap.josm.Main;
-import org.openstreetmap.josm.gui.util.GuiHelper;
+import org.openstreetmap.josm.gui.datatransfer.ClipboardUtils;
 import org.openstreetmap.josm.gui.widgets.JosmComboBox;
-import org.openstreetmap.josm.tools.Utils;
 
 /**
  * Auto-completing ComboBox.
@@ -133,9 +132,9 @@ public class AutoCompletingComboBox extends JosmComboBox<AutoCompletionListItem>
             }
             final JTextComponent editorComponent = comboBox.getEditorComponent();
             // save unix system selection (middle mouse paste)
-            Clipboard sysSel = GuiHelper.getSystemSelection();
+            Clipboard sysSel = ClipboardUtils.getSystemSelection();
             if (sysSel != null) {
-                Transferable old = Utils.getTransferableContent(sysSel);
+                Transferable old = ClipboardUtils.getClipboardContent(sysSel);
                 editorComponent.select(start, end);
                 if (old != null) {
                     sysSel.setContents(old, null);
@@ -200,9 +199,9 @@ public class AutoCompletingComboBox extends JosmComboBox<AutoCompletionListItem>
                             Main.map.keyDetector.setEnabled(false);
                         }
                         // save unix system selection (middle mouse paste)
-                        Clipboard sysSel = GuiHelper.getSystemSelection();
+                        Clipboard sysSel = ClipboardUtils.getSystemSelection();
                         if (sysSel != null) {
-                            Transferable old = Utils.getTransferableContent(sysSel);
+                            Transferable old = ClipboardUtils.getClipboardContent(sysSel);
                             editorComponent.selectAll();
                             if (old != null) {
                                 sysSel.setContents(old, null);
diff --git a/src/org/openstreetmap/josm/gui/util/GuiHelper.java b/src/org/openstreetmap/josm/gui/util/GuiHelper.java
index b6612e4..b61c406 100644
--- a/src/org/openstreetmap/josm/gui/util/GuiHelper.java
+++ b/src/org/openstreetmap/josm/gui/util/GuiHelper.java
@@ -20,7 +20,6 @@ import java.awt.Image;
 import java.awt.Stroke;
 import java.awt.Toolkit;
 import java.awt.Window;
-import java.awt.datatransfer.Clipboard;
 import java.awt.event.ActionListener;
 import java.awt.event.HierarchyEvent;
 import java.awt.event.HierarchyListener;
@@ -496,18 +495,6 @@ public final class GuiHelper {
     }
 
     /**
-     * Gets the singleton instance of the system selection as a <code>Clipboard</code> object.
-     * This allows an application to read and modify the current, system-wide selection.
-     * @return the system selection as a <code>Clipboard</code>, or <code>null</code> if the native platform does not
-     *         support a system selection <code>Clipboard</code> or if GraphicsEnvironment.isHeadless() returns true
-     * @see Toolkit#getSystemSelection
-     * @since 9576
-     */
-    public static Clipboard getSystemSelection() {
-        return GraphicsEnvironment.isHeadless() ? null : Toolkit.getDefaultToolkit().getSystemSelection();
-    }
-
-    /**
      * Returns the first <code>Window</code> ancestor of event source, or
      * {@code null} if event source is not a component contained inside a <code>Window</code>.
      * @param e event object
diff --git a/src/org/openstreetmap/josm/gui/widgets/AbstractIdTextField.java b/src/org/openstreetmap/josm/gui/widgets/AbstractIdTextField.java
index a9d1f33..5a695ed 100644
--- a/src/org/openstreetmap/josm/gui/widgets/AbstractIdTextField.java
+++ b/src/org/openstreetmap/josm/gui/widgets/AbstractIdTextField.java
@@ -4,6 +4,7 @@ package org.openstreetmap.josm.gui.widgets;
 import javax.swing.text.JTextComponent;
 
 import org.openstreetmap.josm.Main;
+import org.openstreetmap.josm.gui.datatransfer.ClipboardUtils;
 import org.openstreetmap.josm.tools.Utils;
 
 /**
@@ -70,7 +71,7 @@ public abstract class AbstractIdTextField<T extends AbstractTextComponentValidat
      * Tries to set text from clipboard (no effect with invalid or empty clipboard)
      */
     public void tryToPasteFromClipboard() {
-        tryToPasteFrom(Utils.getClipboardContent());
+        tryToPasteFrom(ClipboardUtils.getClipboardStringContent());
     }
 
     /**
diff --git a/src/org/openstreetmap/josm/gui/widgets/UrlLabel.java b/src/org/openstreetmap/josm/gui/widgets/UrlLabel.java
index fbb21e4..6a5ac58 100644
--- a/src/org/openstreetmap/josm/gui/widgets/UrlLabel.java
+++ b/src/org/openstreetmap/josm/gui/widgets/UrlLabel.java
@@ -10,8 +10,8 @@ import java.awt.event.MouseListener;
 import javax.swing.JLabel;
 import javax.swing.SwingUtilities;
 
+import org.openstreetmap.josm.gui.datatransfer.ClipboardUtils;
 import org.openstreetmap.josm.tools.OpenBrowser;
-import org.openstreetmap.josm.tools.Utils;
 
 /**
  * Label that contains a clickable link.
@@ -111,7 +111,7 @@ public class UrlLabel extends JLabel implements MouseListener {
         if (SwingUtilities.isLeftMouseButton(e)) {
             OpenBrowser.displayUrl(url);
         } else if (SwingUtilities.isRightMouseButton(e)) {
-            Utils.copyToClipboard(url);
+            ClipboardUtils.copyString(url);
         }
     }
 
diff --git a/src/org/openstreetmap/josm/tools/TextTagParser.java b/src/org/openstreetmap/josm/tools/TextTagParser.java
index a6aa70b..c28c092 100644
--- a/src/org/openstreetmap/josm/tools/TextTagParser.java
+++ b/src/org/openstreetmap/josm/tools/TextTagParser.java
@@ -18,6 +18,7 @@ import javax.swing.JPanel;
 
 import org.openstreetmap.josm.Main;
 import org.openstreetmap.josm.gui.ExtendedDialog;
+import org.openstreetmap.josm.gui.datatransfer.ClipboardUtils;
 import org.openstreetmap.josm.gui.help.HelpUtil;
 import org.openstreetmap.josm.gui.widgets.UrlLabel;
 import org.openstreetmap.josm.io.XmlWriter;
@@ -290,7 +291,7 @@ public final class TextTagParser {
         int r = ed.getValue();
         if (r == 0) r = 2;
         // clean clipboard if user asked
-        if (r == 3) Utils.copyToClipboard("");
+        if (r == 3) ClipboardUtils.copyString("");
         return r;
     }
 
@@ -325,6 +326,6 @@ public final class TextTagParser {
 
         int r = ed.getValue();
         // clean clipboard if user asked
-        if (r == 2) Utils.copyToClipboard("");
+        if (r == 2) ClipboardUtils.copyString("");
     }
 }
diff --git a/src/org/openstreetmap/josm/tools/Utils.java b/src/org/openstreetmap/josm/tools/Utils.java
index bc151aa..ef320a9 100644
--- a/src/org/openstreetmap/josm/tools/Utils.java
+++ b/src/org/openstreetmap/josm/tools/Utils.java
@@ -7,14 +7,8 @@ import static org.openstreetmap.josm.tools.I18n.trn;
 
 import java.awt.Color;
 import java.awt.Font;
-import java.awt.HeadlessException;
-import java.awt.Toolkit;
 import java.awt.datatransfer.Clipboard;
-import java.awt.datatransfer.ClipboardOwner;
-import java.awt.datatransfer.DataFlavor;
-import java.awt.datatransfer.StringSelection;
 import java.awt.datatransfer.Transferable;
-import java.awt.datatransfer.UnsupportedFlavorException;
 import java.awt.font.FontRenderContext;
 import java.awt.font.GlyphVector;
 import java.io.BufferedReader;
@@ -71,6 +65,7 @@ import javax.xml.parsers.SAXParserFactory;
 
 import org.apache.commons.compress.compressors.bzip2.BZip2CompressorInputStream;
 import org.openstreetmap.josm.Main;
+import org.openstreetmap.josm.gui.datatransfer.ClipboardUtils;
 import org.w3c.dom.Document;
 import org.xml.sax.InputSource;
 import org.xml.sax.SAXException;
@@ -623,20 +618,11 @@ public final class Utils {
      * Copies the string {@code s} to system clipboard.
      * @param s string to be copied to clipboard.
      * @return true if succeeded, false otherwise.
+     * @deprecated Use {@link ClipboardUtils#copyString(String)}. To be removed end of 2016.
      */
+    @Deprecated
     public static boolean copyToClipboard(String s) {
-        try {
-            Toolkit.getDefaultToolkit().getSystemClipboard().setContents(new StringSelection(s), new ClipboardOwner() {
-                @Override
-                public void lostOwnership(Clipboard clpbrd, Transferable t) {
-                    // Do nothing
-                }
-            });
-            return true;
-        } catch (IllegalStateException | HeadlessException ex) {
-            Main.error(ex);
-            return false;
-        }
+        return ClipboardUtils.copyString(s);
     }
 
     /**
@@ -644,43 +630,21 @@ public final class Utils {
      * @param clipboard clipboard from which contents are retrieved
      * @return clipboard contents if available, {@code null} otherwise.
      * @since 8429
+     * @deprecated Use {@link ClipboardUtils#getClipboardContent(Clipboard)} instead. To be removed end of 2016.
      */
+    @Deprecated
     public static Transferable getTransferableContent(Clipboard clipboard) {
-        Transferable t = null;
-        for (int tries = 0; t == null && tries < 10; tries++) {
-            try {
-                t = clipboard.getContents(null);
-            } catch (IllegalStateException e) {
-                // Clipboard currently unavailable.
-                // On some platforms, the system clipboard is unavailable while it is accessed by another application.
-                try {
-                    Thread.sleep(1);
-                } catch (InterruptedException ex) {
-                    Main.warn("InterruptedException in "+Utils.class.getSimpleName()+" while getting clipboard content");
-                }
-            } catch (NullPointerException e) {
-                // JDK-6322854: On Linux/X11, NPE can happen for unknown reasons, on all versions of Java
-                Main.error(e);
-            }
-        }
-        return t;
+        return ClipboardUtils.getClipboardContent(clipboard);
     }
 
     /**
      * Extracts clipboard content as string.
      * @return string clipboard contents if available, {@code null} otherwise.
+     * @deprecated Use {@link ClipboardUtils#getClipboardStringContent()}. To be removed end of 2016
      */
+    @Deprecated
     public static String getClipboardContent() {
-        try {
-            Transferable t = getTransferableContent(Toolkit.getDefaultToolkit().getSystemClipboard());
-            if (t != null && t.isDataFlavorSupported(DataFlavor.stringFlavor)) {
-                return (String) t.getTransferData(DataFlavor.stringFlavor);
-            }
-        } catch (UnsupportedFlavorException | IOException | HeadlessException ex) {
-            Main.error(ex);
-            return null;
-        }
-        return null;
+        return ClipboardUtils.getClipboardStringContent();
     }
 
     /**
diff --git a/src/org/openstreetmap/josm/tools/bugreport/DebugTextDisplay.java b/src/org/openstreetmap/josm/tools/bugreport/DebugTextDisplay.java
index 28f88ac..237b952 100644
--- a/src/org/openstreetmap/josm/tools/bugreport/DebugTextDisplay.java
+++ b/src/org/openstreetmap/josm/tools/bugreport/DebugTextDisplay.java
@@ -5,6 +5,7 @@ import java.awt.Dimension;
 
 import javax.swing.JScrollPane;
 
+import org.openstreetmap.josm.gui.datatransfer.ClipboardUtils;
 import org.openstreetmap.josm.gui.widgets.JosmTextArea;
 import org.openstreetmap.josm.tools.Utils;
 
@@ -34,6 +35,6 @@ public class DebugTextDisplay extends JScrollPane {
      * @return <code>true</code> if copy was successful
      */
     public boolean copyToClippboard() {
-        return Utils.copyToClipboard(text);
+        return ClipboardUtils.copyString(text);
     }
 }
diff --git a/test/unit/org/openstreetmap/josm/actions/CopyActionTest.java b/test/unit/org/openstreetmap/josm/actions/CopyActionTest.java
index 426b4e7..05758b9 100644
--- a/test/unit/org/openstreetmap/josm/actions/CopyActionTest.java
+++ b/test/unit/org/openstreetmap/josm/actions/CopyActionTest.java
@@ -2,46 +2,119 @@
 package org.openstreetmap.josm.actions;
 
 import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.fail;
 
+import java.awt.datatransfer.Clipboard;
+import java.awt.datatransfer.DataFlavor;
+import java.awt.datatransfer.StringSelection;
+import java.awt.datatransfer.UnsupportedFlavorException;
+import java.io.IOException;
 import java.util.Arrays;
-import java.util.Collections;
 
-import org.junit.BeforeClass;
+import org.junit.Rule;
 import org.junit.Test;
-import org.openstreetmap.josm.JOSMFixture;
-import org.openstreetmap.josm.data.osm.Relation;
+import org.openstreetmap.josm.Main;
+import org.openstreetmap.josm.data.coor.LatLon;
+import org.openstreetmap.josm.data.osm.DataSet;
+import org.openstreetmap.josm.data.osm.Node;
 import org.openstreetmap.josm.data.osm.Way;
+import org.openstreetmap.josm.gui.datatransfer.ClipboardUtils;
+import org.openstreetmap.josm.gui.datatransfer.data.PrimitiveTransferData;
+import org.openstreetmap.josm.gui.layer.OsmDataLayer;
+import org.openstreetmap.josm.testutils.JOSMTestRules;
+
+import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
 
 /**
  * Unit tests for class {@link CopyAction}.
  */
 public class CopyActionTest {
+    private final class CapturingCopyAction extends CopyAction {
+        private boolean warningShown;
+
+        @Override
+        protected void showEmptySelectionWarning() {
+            warningShown = true;
+        }
+    }
 
     /**
-     * Setup test.
+     * We need prefs for this.
      */
-    @BeforeClass
-    public static void setUpBeforeClass() {
-        JOSMFixture.createUnitTestFixture().init();
-    }
+    @Rule
+    @SuppressFBWarnings(value = "URF_UNREAD_PUBLIC_OR_PROTECTED_FIELD")
+    public JOSMTestRules test = new JOSMTestRules().preferences().platform().fakeAPI();
 
     /**
-     * Test of {@link CopyAction#getCopyString} method for a single way.
+     * Test that copy action copies the selected primitive
+     * @throws IOException
+     * @throws UnsupportedFlavorException
      */
     @Test
-    public void testCopyStringWay() {
-        final Way way = new Way(123L);
-        assertEquals("way 123", CopyAction.getCopyString(Collections.singleton(way)));
-    }
+    public void testWarnOnEmpty() throws UnsupportedFlavorException, IOException {
+        Clipboard clippboard = ClipboardUtils.getClipboard();
+        clippboard.setContents(new StringSelection("test"), null);
+
+        CapturingCopyAction action = new CapturingCopyAction();
+
+        action.updateEnabledState();
+        assertFalse(action.isEnabled());
+        action.actionPerformed(null);
+        assertTrue(action.warningShown);
+
+        Main.getLayerManager().addLayer(new OsmDataLayer(new DataSet(), "test", null));
+        action.warningShown = false;
 
+        action.updateEnabledState();
+        assertFalse(action.isEnabled());
+        action.actionPerformed(null);
+        assertTrue(action.warningShown);
+
+        assertEquals("test", clippboard.getContents(null).getTransferData(DataFlavor.stringFlavor));
+    }
     /**
-     * Test of {@link CopyAction#getCopyString} method for a way and a relation.
+     * Test that copy action copies the selected primitive
+     * @throws IOException
+     * @throws UnsupportedFlavorException
      */
     @Test
-    public void testCopyStringWayRelation() {
-        final Way way = new Way(123L);
-        final Relation relation = new Relation(456);
-        assertEquals("way 123,relation 456", CopyAction.getCopyString(Arrays.asList(way, relation)));
-        assertEquals("relation 456,way 123", CopyAction.getCopyString(Arrays.asList(relation, way)));
+    public void testCopySinglePrimitive() throws UnsupportedFlavorException, IOException {
+        DataSet data = new DataSet();
+
+        Node node1 = new Node();
+        node1.setCoor(LatLon.ZERO);
+        data.addPrimitive(node1);
+
+        Node node2 = new Node();
+        node2.setCoor(LatLon.ZERO);
+        data.addPrimitive(node2);
+        Way way = new Way();
+        way.setNodes(Arrays.asList(node1, node2));
+        data.addPrimitive(way);
+        data.setSelected(way);
+
+        Main.getLayerManager().addLayer(new OsmDataLayer(data, "test", null));
+
+        CopyAction action = new CopyAction() {
+            @Override
+            protected void showEmptySelectionWarning() {
+                fail("Selection is not empty.");
+            }
+        };
+        action.updateEnabledState();
+        assertTrue(action.isEnabled());
+        action.actionPerformed(null);
+
+        Object copied = ClipboardUtils.getClipboard().getContents(null).getTransferData(PrimitiveTransferData.DATA_FLAVOR);
+        assertNotNull(copied);
+        assertTrue(copied instanceof PrimitiveTransferData);
+        PrimitiveTransferData ptd = (PrimitiveTransferData) copied;
+        Object[] direct = ptd.getDirectlyAdded().toArray();
+        assertEquals(1, direct.length);
+        Object[] referenced = ptd.getReferenced().toArray();
+        assertEquals(2, referenced.length);
     }
 }
diff --git a/test/unit/org/openstreetmap/josm/gui/datatransfer/ClipboardUtilsTest.java b/test/unit/org/openstreetmap/josm/gui/datatransfer/ClipboardUtilsTest.java
new file mode 100644
index 0000000..fc4be5a
--- /dev/null
+++ b/test/unit/org/openstreetmap/josm/gui/datatransfer/ClipboardUtilsTest.java
@@ -0,0 +1,123 @@
+// License: GPL. For details, see LICENSE file.
+package org.openstreetmap.josm.gui.datatransfer;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertSame;
+import static org.junit.Assert.assertTrue;
+
+import java.awt.GraphicsEnvironment;
+import java.awt.datatransfer.Clipboard;
+import java.awt.datatransfer.DataFlavor;
+import java.awt.datatransfer.StringSelection;
+import java.awt.datatransfer.Transferable;
+import java.awt.datatransfer.UnsupportedFlavorException;
+import java.io.IOException;
+
+import org.junit.Rule;
+import org.junit.Test;
+import org.openstreetmap.josm.testutils.JOSMTestRules;
+
+import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
+
+/**
+ * Basic tests for the clipboard utils class.
+ * @author Michael Zangl
+ * @since xxx
+ */
+public class ClipboardUtilsTest {
+    private final static class ThrowIllegalStateClipboard extends Clipboard {
+        private int failingAccesses = 3;
+
+        private ThrowIllegalStateClipboard(String name) {
+            super(name);
+        }
+
+        @Override
+        public synchronized Transferable getContents(Object requestor) {
+            if (failingAccesses >= 0) {
+                failingAccesses--;
+                throw new IllegalStateException();
+            }
+            return super.getContents(requestor);
+        }
+
+        protected synchronized void setFailingAccesses(int failingAccesses) {
+            this.failingAccesses = failingAccesses;
+        }
+    }
+
+    private final static class SupportNothingTransferable implements Transferable {
+        @Override
+        public boolean isDataFlavorSupported(DataFlavor flavor) {
+            return false;
+        }
+
+        @Override
+        public DataFlavor[] getTransferDataFlavors() {
+            return new DataFlavor[0];
+        }
+
+        @Override
+        public Object getTransferData(DataFlavor flavor) throws UnsupportedFlavorException, IOException {
+            throw new UnsupportedFlavorException(flavor);
+        }
+    }
+
+    /**
+     * No dependencies
+     */
+    @Rule
+    @SuppressFBWarnings(value = "URF_UNREAD_PUBLIC_OR_PROTECTED_FIELD")
+    public JOSMTestRules test = new JOSMTestRules();
+
+    /**
+     * Test {@link ClipboardUtils#getClipboard()}
+     */
+    @Test
+    public void testGetClipboard() {
+        Clipboard c = ClipboardUtils.getClipboard();
+        assertNotNull(c);
+        assertSame(c, ClipboardUtils.getClipboard());
+    }
+
+    /**
+     * Test {@link ClipboardUtils#copyString(String)} and {@link ClipboardUtils#getClipboardStringContent()}
+     */
+    @Test
+    public void testCopyPasteString() {
+        ClipboardUtils.copyString("");
+        assertEquals("", ClipboardUtils.getClipboardStringContent());
+        ClipboardUtils.copyString("xxx\nx");
+        assertEquals("xxx\nx", ClipboardUtils.getClipboardStringContent());
+
+        ClipboardUtils.copy(new SupportNothingTransferable());
+        assertEquals(null, ClipboardUtils.getClipboardStringContent());
+    }
+
+    /**
+     * Test that {@link ClipboardUtils#getClipboardContent(Clipboard)} handles illegal state exceptions
+     */
+    @Test
+    public void testGetContentIllegalState() {
+        ThrowIllegalStateClipboard throwingClipboard = new ThrowIllegalStateClipboard("test");
+
+        throwingClipboard.setContents(new StringSelection(""), null);
+        Transferable content = ClipboardUtils.getClipboardContent(throwingClipboard);
+        assertTrue(content.isDataFlavorSupported(DataFlavor.stringFlavor));
+
+        throwingClipboard.setFailingAccesses(50);
+        content = ClipboardUtils.getClipboardContent(new ThrowIllegalStateClipboard("test"));
+        assertNull(content);
+    }
+
+    /**
+     * Test that {@link ClipboardUtils#getSystemSelection()} works in headless mode.
+     */
+    @Test
+    public void testSystemSelectionDoesNotFail() {
+        assertTrue(GraphicsEnvironment.isHeadless());
+        assertNull(ClipboardUtils.getSystemSelection());
+    }
+}
diff --git a/test/unit/org/openstreetmap/josm/gui/datatransfer/PrimitiveTransferableTest.java b/test/unit/org/openstreetmap/josm/gui/datatransfer/PrimitiveTransferableTest.java
index 69ad859..2c708ca 100644
--- a/test/unit/org/openstreetmap/josm/gui/datatransfer/PrimitiveTransferableTest.java
+++ b/test/unit/org/openstreetmap/josm/gui/datatransfer/PrimitiveTransferableTest.java
@@ -4,41 +4,52 @@ package org.openstreetmap.josm.gui.datatransfer;
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertFalse;
 import static org.junit.Assert.assertTrue;
-import static org.openstreetmap.josm.gui.datatransfer.PrimitiveTransferable.PRIMITIVE_DATA;
 
 import java.awt.datatransfer.DataFlavor;
 import java.awt.datatransfer.UnsupportedFlavorException;
+import java.util.Arrays;
 import java.util.Collection;
 import java.util.Collections;
+import java.util.List;
 
-import org.junit.BeforeClass;
+import org.junit.Rule;
 import org.junit.Test;
-import org.openstreetmap.josm.JOSMFixture;
 import org.openstreetmap.josm.data.osm.Node;
+import org.openstreetmap.josm.data.osm.NodeData;
 import org.openstreetmap.josm.data.osm.PrimitiveData;
+import org.openstreetmap.josm.gui.datatransfer.data.PrimitiveTransferData;
+import org.openstreetmap.josm.gui.datatransfer.data.TagTransferData;
+import org.openstreetmap.josm.testutils.JOSMTestRules;
+
+import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
 
 /**
  * Unit tests of {@link PrimitiveTransferable} class.
  */
 public class PrimitiveTransferableTest {
-
     /**
-     * Setup tests
+     * Prefs to use OSM primitives
      */
-    @BeforeClass
-    public static void setUpBeforeClass() {
-        JOSMFixture.createUnitTestFixture().init();
-    }
+    @Rule
+    @SuppressFBWarnings(value = "URF_UNREAD_PUBLIC_OR_PROTECTED_FIELD")
+    public JOSMTestRules test = new JOSMTestRules().preferences();
 
     /**
-     * Test of {@link PrimitiveTransferable#getTransferDataFlavors()} method.
+     * Test of {@link PrimitiveTransferable#getTransferDataFlavors()} method response order
      */
     @Test
     public void testGetTransferDataFlavors() {
-        DataFlavor[] flavors = new PrimitiveTransferable(null).getTransferDataFlavors();
-        assertEquals(2, flavors.length);
-        assertEquals(PRIMITIVE_DATA, flavors[0]);
-        assertEquals(DataFlavor.stringFlavor, flavors[1]);
+        List<DataFlavor> flavors = Arrays.asList(new PrimitiveTransferable(null).getTransferDataFlavors());
+        int ptd = flavors.indexOf(PrimitiveTransferData.DATA_FLAVOR);
+        int tags = flavors.indexOf(TagTransferData.FLAVOR);
+        int string = flavors.indexOf(DataFlavor.stringFlavor);
+
+        assertTrue(ptd >= 0);
+        assertTrue(tags >= 0);
+        assertTrue(string >= 0);
+
+        assertTrue(ptd < tags);
+        assertTrue(tags < string);
     }
 
     /**
@@ -46,8 +57,8 @@ public class PrimitiveTransferableTest {
      */
     @Test
     public void testIsDataFlavorSupported() {
-        assertTrue(new PrimitiveTransferable(null).isDataFlavorSupported(PRIMITIVE_DATA));
-        assertFalse(new PrimitiveTransferable(null).isDataFlavorSupported(null));
+        assertTrue(new PrimitiveTransferable(null).isDataFlavorSupported(PrimitiveTransferData.DATA_FLAVOR));
+        assertFalse(new PrimitiveTransferable(null).isDataFlavorSupported(DataFlavor.imageFlavor));
     }
 
     /**
@@ -56,11 +67,17 @@ public class PrimitiveTransferableTest {
      */
     @Test
     public void testGetTransferDataNominal() throws UnsupportedFlavorException {
-        PrimitiveTransferable pt = new PrimitiveTransferable(Collections.singleton(new Node(1)));
-        assertEquals("node 1 # incomplete\n", pt.getTransferData(DataFlavor.stringFlavor));
-        Collection<PrimitiveData> td = ((PrimitiveTransferable.Data) pt.getTransferData(PRIMITIVE_DATA)).getPrimitiveData();
+        PrimitiveTransferData data = PrimitiveTransferData.getData(Collections.singleton(new Node(1)));
+        PrimitiveTransferable pt = new PrimitiveTransferable(data);
+        assertEquals("node 1", pt.getTransferData(DataFlavor.stringFlavor));
+        Collection<PrimitiveData> td = ((PrimitiveTransferData) pt.getTransferData(PrimitiveTransferData.DATA_FLAVOR)).getAll();
         assertEquals(1, td.size());
-        assertTrue(td.iterator().next() instanceof PrimitiveData);
+        assertTrue(td.iterator().next() instanceof NodeData);
+
+
+        data = PrimitiveTransferData.getData(Arrays.asList(new Node(1), new Node(2)));
+        pt = new PrimitiveTransferable(data);
+        assertEquals("node 1\nnode 2", pt.getTransferData(DataFlavor.stringFlavor));
     }
 
     /**
@@ -69,6 +86,7 @@ public class PrimitiveTransferableTest {
      */
     @Test(expected = UnsupportedFlavorException.class)
     public void testGetTransferDataError() throws UnsupportedFlavorException {
-        new PrimitiveTransferable(Collections.singleton(new Node(1))).getTransferData(null);
+        PrimitiveTransferData data = PrimitiveTransferData.getData(Collections.singleton(new Node(1)));
+        new PrimitiveTransferable(data).getTransferData(DataFlavor.imageFlavor);
     }
 }
diff --git a/test/unit/org/openstreetmap/josm/gui/datatransfer/RelationMemberTransferableTest.java b/test/unit/org/openstreetmap/josm/gui/datatransfer/RelationMemberTransferableTest.java
index 477fbf0..1fa90e5 100644
--- a/test/unit/org/openstreetmap/josm/gui/datatransfer/RelationMemberTransferableTest.java
+++ b/test/unit/org/openstreetmap/josm/gui/datatransfer/RelationMemberTransferableTest.java
@@ -37,7 +37,7 @@ public class RelationMemberTransferableTest {
      */
     @Test
     public void testGetTransferDataFlavors() {
-        DataFlavor[] flavors = new RelationMemberTransferable(null).getTransferDataFlavors();
+        DataFlavor[] flavors = new RelationMemberTransferable(Collections.<RelationMember>emptyList()).getTransferDataFlavors();
         assertEquals(2, flavors.length);
         assertEquals(RELATION_MEMBER_DATA, flavors[0]);
         assertEquals(DataFlavor.stringFlavor, flavors[1]);
@@ -48,8 +48,9 @@ public class RelationMemberTransferableTest {
      */
     @Test
     public void testIsDataFlavorSupported() {
-        assertTrue(new RelationMemberTransferable(null).isDataFlavorSupported(RELATION_MEMBER_DATA));
-        assertFalse(new RelationMemberTransferable(null).isDataFlavorSupported(null));
+        RelationMemberTransferable transferable = new RelationMemberTransferable(Collections.<RelationMember>emptyList());
+        assertTrue(transferable.isDataFlavorSupported(RELATION_MEMBER_DATA));
+        assertFalse(transferable.isDataFlavorSupported(null));
     }
 
     /**
diff --git a/test/unit/org/openstreetmap/josm/gui/dialogs/relation/actions/AbstractRelationEditorActionTest.java b/test/unit/org/openstreetmap/josm/gui/dialogs/relation/actions/AbstractRelationEditorActionTest.java
new file mode 100644
index 0000000..cf48256
--- /dev/null
+++ b/test/unit/org/openstreetmap/josm/gui/dialogs/relation/actions/AbstractRelationEditorActionTest.java
@@ -0,0 +1,73 @@
+// License: GPL. For details, see LICENSE file.
+package org.openstreetmap.josm.gui.dialogs.relation.actions;
+
+import java.util.Collection;
+import java.util.Collections;
+import java.util.List;
+
+import org.junit.Before;
+import org.junit.Rule;
+import org.openstreetmap.josm.data.osm.DataSet;
+import org.openstreetmap.josm.data.osm.OsmPrimitive;
+import org.openstreetmap.josm.data.osm.Relation;
+import org.openstreetmap.josm.data.osm.Tag;
+import org.openstreetmap.josm.gui.dialogs.relation.GenericRelationEditorTest;
+import org.openstreetmap.josm.gui.dialogs.relation.IRelationEditor;
+import org.openstreetmap.josm.gui.dialogs.relation.MemberTable;
+import org.openstreetmap.josm.gui.dialogs.relation.MemberTableModel;
+import org.openstreetmap.josm.gui.dialogs.relation.SelectionTableModel;
+import org.openstreetmap.josm.gui.layer.OsmDataLayer;
+import org.openstreetmap.josm.gui.tagging.presets.TaggingPresetHandler;
+import org.openstreetmap.josm.testutils.JOSMTestRules;
+
+import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
+
+/**
+ * This class provides the basic test environment for relation editor actions.
+ * @author Michael Zangl
+ * @since xxx
+ */
+public abstract class AbstractRelationEditorActionTest {
+    /**
+     * Plattform for tooltips.
+     */
+    @Rule
+    @SuppressFBWarnings(value = "URF_UNREAD_PUBLIC_OR_PROTECTED_FIELD")
+    public JOSMTestRules test = new JOSMTestRules().preferences().platform();
+
+    protected SelectionTableModel selectionTableModel;
+
+    protected IRelationEditor editor;
+
+    protected MemberTable memberTable;
+
+    protected OsmDataLayer layer;
+
+    protected MemberTableModel memberTableModel;
+
+    /**
+     * Set up the test data required for common tests using one relation.
+     */
+    @Before
+    public void setupTestData() {
+        DataSet ds = new DataSet();
+        final Relation orig = new Relation(1);
+        ds.addPrimitive(orig);
+        layer = new OsmDataLayer(ds, "test", null);
+        memberTableModel = new MemberTableModel(orig, layer, new TaggingPresetHandler() {
+            @Override
+            public void updateTags(List<Tag> tags) {
+            }
+
+            @Override
+            public Collection<OsmPrimitive> getSelection() {
+                return Collections.<OsmPrimitive>singleton(orig);
+            }
+        });
+        selectionTableModel = new SelectionTableModel(layer);
+
+        editor = GenericRelationEditorTest.newRelationEditor(orig, layer);
+
+        memberTable = new MemberTable(layer, editor.getRelation(), memberTableModel);
+    }
+}
diff --git a/test/unit/org/openstreetmap/josm/gui/dialogs/relation/actions/PasteMembersActionTest.java b/test/unit/org/openstreetmap/josm/gui/dialogs/relation/actions/PasteMembersActionTest.java
new file mode 100644
index 0000000..40e9156
--- /dev/null
+++ b/test/unit/org/openstreetmap/josm/gui/dialogs/relation/actions/PasteMembersActionTest.java
@@ -0,0 +1,130 @@
+// License: GPL. For details, see LICENSE file.
+package org.openstreetmap.josm.gui.dialogs.relation.actions;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertSame;
+import static org.junit.Assert.assertTrue;
+
+import java.util.Collections;
+import java.util.Set;
+
+import org.junit.Test;
+import org.openstreetmap.josm.data.osm.Node;
+import org.openstreetmap.josm.data.osm.Relation;
+import org.openstreetmap.josm.data.osm.RelationMember;
+import org.openstreetmap.josm.gui.datatransfer.ClipboardUtils;
+import org.openstreetmap.josm.gui.datatransfer.PrimitiveTransferable;
+import org.openstreetmap.josm.gui.datatransfer.RelationMemberTransferable;
+import org.openstreetmap.josm.gui.datatransfer.data.PrimitiveTransferData;
+import org.openstreetmap.josm.gui.util.GuiHelper;
+
+/**
+ * Test for {@link PasteMembersAction}
+ * @author Michael Zangl
+ * @since xxx
+ */
+public class PasteMembersActionTest extends AbstractRelationEditorActionTest {
+    /**
+     * Test {@link PasteMembersAction#isEnabled()}
+     */
+    @Test
+    public void testEnabledState() {
+        copyString();
+
+        PasteMembersAction action = new PasteMembersAction(memberTable, layer, editor);
+        ClipboardUtils.getClipboard().addFlavorListener(action);
+
+        try {
+            assertFalse(action.isEnabled());
+
+            Node node = new Node();
+            copyNode(node);
+            syncListener();
+            assertTrue(action.isEnabled());
+
+            copyMember(node);
+            syncListener();
+            assertTrue(action.isEnabled());
+
+            copyString();
+            syncListener();
+            assertFalse(action.isEnabled());
+        } finally {
+            ClipboardUtils.getClipboard().removeFlavorListener(action);
+        }
+    }
+
+    private void syncListener() {
+        GuiHelper.runInEDTAndWait(new Runnable() {
+            @Override
+            public void run() {
+                // nop
+            }
+        });
+    }
+
+    /**
+     * Test that pasting produces the result required
+     */
+    @Test
+    public void testActionWrongClipboard() {
+        copyString();
+        PasteMembersAction action = new PasteMembersAction(memberTable, layer, editor);
+        action.actionPerformed(null);
+
+        Relation relation = new Relation(1);
+        memberTableModel.applyToRelation(relation);
+        assertEquals(0, relation.getMembersCount());
+    }
+
+    /**
+     * Test that pasting produces the result required
+     */
+    @Test
+    public void testActionForMembers() {
+        Node testNode = new Node(10);
+        layer.data.addPrimitive(testNode);
+        copyMember(testNode);
+        PasteMembersAction action = new PasteMembersAction(memberTable, layer, editor);
+        action.actionPerformed(null);
+
+        Relation relation = new Relation(1);
+        memberTableModel.applyToRelation(relation);
+        assertEquals(1, relation.getMembersCount());
+        assertEquals("test", relation.getMember(0).getRole());
+        assertSame(testNode, relation.getMember(0).getMember());
+    }
+
+    /**
+     * Test that pasting primitvies produces the result required
+     */
+    @Test
+    public void testActionForPrimitives() {
+        Node testNode = new Node(10);
+        layer.data.addPrimitive(testNode);
+        copyNode(testNode);
+        PasteMembersAction action = new PasteMembersAction(memberTable, layer, editor);
+        action.actionPerformed(null);
+
+        Relation relation = new Relation(1);
+        memberTableModel.applyToRelation(relation);
+        assertEquals(1, relation.getMembersCount());
+        assertEquals("", relation.getMember(0).getRole());
+        assertSame(testNode, relation.getMember(0).getMember());
+    }
+
+    private void copyNode(Node node) {
+        PrimitiveTransferData data = PrimitiveTransferData.getData(Collections.singleton(node));
+        ClipboardUtils.copy(new PrimitiveTransferable(data));
+    }
+
+    private void copyMember(Node node) {
+        Set<RelationMember> members = Collections.singleton(new RelationMember("test", node));
+        ClipboardUtils.copy(new RelationMemberTransferable(members));
+    }
+
+    private void copyString() {
+        ClipboardUtils.copyString("");
+    }
+}
diff --git a/test/unit/org/openstreetmap/josm/gui/dialogs/relation/actions/RelationEditorActionsTest.java b/test/unit/org/openstreetmap/josm/gui/dialogs/relation/actions/RelationEditorActionsTest.java
index f44f7fb..bc5a1ab 100644
--- a/test/unit/org/openstreetmap/josm/gui/dialogs/relation/actions/RelationEditorActionsTest.java
+++ b/test/unit/org/openstreetmap/josm/gui/dialogs/relation/actions/RelationEditorActionsTest.java
@@ -1,9 +1,8 @@
 // License: GPL. For details, see LICENSE file.
 package org.openstreetmap.josm.gui.dialogs.relation.actions;
 
-import org.junit.BeforeClass;
+import org.junit.Rule;
 import org.junit.Test;
-import org.openstreetmap.josm.JOSMFixture;
 import org.openstreetmap.josm.data.osm.DataSet;
 import org.openstreetmap.josm.data.osm.Relation;
 import org.openstreetmap.josm.gui.dialogs.relation.GenericRelationEditorTest;
@@ -14,22 +13,23 @@ import org.openstreetmap.josm.gui.dialogs.relation.SelectionTableModel;
 import org.openstreetmap.josm.gui.layer.OsmDataLayer;
 import org.openstreetmap.josm.gui.tagging.TagEditorModel;
 import org.openstreetmap.josm.gui.tagging.ac.AutoCompletingTextField;
+import org.openstreetmap.josm.testutils.JOSMTestRules;
+
+import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
 
 /**
  * Unit tests for relation editor actions.
  */
 public class RelationEditorActionsTest {
-
     /**
-     * Setup test.
+     * Plattform for tooltips.
      */
-    @BeforeClass
-    public static void setUpBeforeClass() {
-        JOSMFixture.createUnitTestFixture().init(true);
-    }
+    @Rule
+    @SuppressFBWarnings(value = "URF_UNREAD_PUBLIC_OR_PROTECTED_FIELD")
+    public JOSMTestRules test = new JOSMTestRules().preferences().platform().commands();
 
     /**
-     * Test all actions with minimal data.
+     * Check that all actions do not crash.
      */
     @Test
     public void testAllActions() {
@@ -57,7 +57,7 @@ public class RelationEditorActionsTest {
         new CancelAction(memberTable, memberTableModel, tagModel, layer, editor, tfRole).actionPerformed(null);
 
         new CopyMembersAction(memberTableModel, layer, editor).actionPerformed(null);
-        new PasteMembersAction(memberTableModel, layer, editor).actionPerformed(null);
+        new PasteMembersAction(memberTable, layer, editor).actionPerformed(null);
 
         new DeleteCurrentRelationAction(layer, editor).actionPerformed(null);
 
diff --git a/test/unit/org/openstreetmap/josm/testutils/JOSMTestRules.java b/test/unit/org/openstreetmap/josm/testutils/JOSMTestRules.java
index 9fba4cd..9b8abb7 100644
--- a/test/unit/org/openstreetmap/josm/testutils/JOSMTestRules.java
+++ b/test/unit/org/openstreetmap/josm/testutils/JOSMTestRules.java
@@ -12,6 +12,7 @@ import org.junit.rules.Timeout;
 import org.junit.runner.Description;
 import org.junit.runners.model.InitializationError;
 import org.junit.runners.model.Statement;
+import org.openstreetmap.josm.JOSMFixture;
 import org.openstreetmap.josm.Main;
 import org.openstreetmap.josm.data.projection.Projections;
 import org.openstreetmap.josm.gui.util.GuiHelper;
@@ -39,6 +40,7 @@ public class JOSMTestRules implements TestRule {
     private String i18n = null;
     private boolean platform;
     private boolean useProjection;
+    private boolean commands;
 
     /**
      * Disable the default timeout for this test. Use with care.
@@ -133,6 +135,15 @@ public class JOSMTestRules implements TestRule {
         return this;
     }
 
+    /**
+     * Allow the execution of commands using {@link Main#undoRedo}
+     * @return this instance, for easy chaining
+     */
+    public JOSMTestRules commands() {
+        commands = true;
+        return this;
+    }
+
     @Override
     public Statement apply(final Statement base, Description description) {
         Statement statement = new Statement() {
@@ -160,10 +171,11 @@ public class JOSMTestRules implements TestRule {
      * @throws InitializationError If an error occured while creating the required environment.
      */
     protected void before() throws InitializationError {
-        cleanUpFromJosmFixture();
-
         // Tests are running headless by default.
         System.setProperty("java.awt.headless", "true");
+
+        cleanUpFromJosmFixture();
+
         // All tests use the same timezone.
         TimeZone.setDefault(DateUtils.UTC);
         // Set log level to info
@@ -218,6 +230,11 @@ public class JOSMTestRules implements TestRule {
         if (platform) {
             Main.determinePlatformHook();
         }
+
+        if (commands) {
+            // TODO: Implement a more slective version of this once Main is restructured.
+            JOSMFixture.createUnitTestFixture().init(true);
+        }
     }
 
     /**
