diff --git a/src/org/openstreetmap/josm/actions/PasteTagsAction.java b/src/org/openstreetmap/josm/actions/PasteTagsAction.java
index d111ab8..a62cadd 100644
--- a/src/org/openstreetmap/josm/actions/PasteTagsAction.java
+++ b/src/org/openstreetmap/josm/actions/PasteTagsAction.java
@@ -3,32 +3,15 @@ package org.openstreetmap.josm.actions;
 
 import static org.openstreetmap.josm.gui.help.HelpUtil.ht;
 import static org.openstreetmap.josm.tools.I18n.tr;
-import static org.openstreetmap.josm.tools.I18n.trn;
 
 import java.awt.event.ActionEvent;
 import java.awt.event.KeyEvent;
-import java.util.ArrayList;
 import java.util.Collection;
-import java.util.EnumMap;
-import java.util.List;
-import java.util.Map;
-import java.util.Map.Entry;
 
-import org.openstreetmap.josm.Main;
-import org.openstreetmap.josm.command.ChangePropertyCommand;
-import org.openstreetmap.josm.command.Command;
-import org.openstreetmap.josm.command.SequenceCommand;
 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.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;
 
 /**
  * Action, to paste all tags from one primitive to another.
@@ -54,205 +37,6 @@ public final class PasteTagsAction extends JosmAction {
         putValue("help", help);
     }
 
-    /**
-     * Used to update the tags.
-     */
-    public static class TagPaster {
-
-        private final Collection<PrimitiveData> source;
-        private final Collection<OsmPrimitive> target;
-        private final List<Tag> tags = new ArrayList<>();
-
-        /**
-         * Constructs a new {@code TagPaster}.
-         * @param source source primitives
-         * @param target target primitives
-         */
-        public TagPaster(Collection<PrimitiveData> source, Collection<OsmPrimitive> target) {
-            this.source = source;
-            this.target = target;
-        }
-
-        /**
-         * Determines if the source for tag pasting is heterogeneous, i.e. if it doesn't consist of
-         * {@link OsmPrimitive}s of exactly one type
-         * @return true if the source for tag pasting is heterogeneous
-         */
-        protected boolean isHeterogeneousSource() {
-            int count = 0;
-            count = !getSourcePrimitivesByType(OsmPrimitiveType.NODE).isEmpty() ? (count + 1) : count;
-            count = !getSourcePrimitivesByType(OsmPrimitiveType.WAY).isEmpty() ? (count + 1) : count;
-            count = !getSourcePrimitivesByType(OsmPrimitiveType.RELATION).isEmpty() ? (count + 1) : count;
-            return count > 1;
-        }
-
-        /**
-         * Replies all primitives of type <code>type</code> in the current selection.
-         *
-         * @param type  the type
-         * @return all primitives of type <code>type</code> in the current selection.
-         */
-        protected Collection<? extends PrimitiveData> getSourcePrimitivesByType(OsmPrimitiveType type) {
-            return PrimitiveData.getFilteredList(source, type);
-        }
-
-        /**
-         * Replies the collection of tags for all primitives of type <code>type</code> in the current
-         * selection
-         *
-         * @param type  the type
-         * @return the collection of tags for all primitives of type <code>type</code> in the current
-         * selection
-         */
-        protected TagCollection getSourceTagsByType(OsmPrimitiveType type) {
-            return TagCollection.unionOfAllPrimitives(getSourcePrimitivesByType(type));
-        }
-
-        /**
-         * Replies true if there is at least one tag in the current selection for primitives of
-         * type <code>type</code>
-         *
-         * @param type the type
-         * @return true if there is at least one tag in the current selection for primitives of
-         * type <code>type</code>
-         */
-        protected boolean hasSourceTagsByType(OsmPrimitiveType type) {
-            return !getSourceTagsByType(type).isEmpty();
-        }
-
-        protected void buildTags(TagCollection tc) {
-            for (String key : tc.getKeys()) {
-                tags.add(new Tag(key, tc.getValues(key).iterator().next()));
-            }
-        }
-
-        protected Map<OsmPrimitiveType, Integer> getSourceStatistics() {
-            Map<OsmPrimitiveType, Integer> ret = new EnumMap<>(OsmPrimitiveType.class);
-            for (OsmPrimitiveType type: OsmPrimitiveType.dataValues()) {
-                if (!getSourceTagsByType(type).isEmpty()) {
-                    ret.put(type, getSourcePrimitivesByType(type).size());
-                }
-            }
-            return ret;
-        }
-
-        protected Map<OsmPrimitiveType, Integer> getTargetStatistics() {
-            Map<OsmPrimitiveType, Integer> ret = new EnumMap<>(OsmPrimitiveType.class);
-            for (OsmPrimitiveType type: OsmPrimitiveType.dataValues()) {
-                int count = OsmPrimitive.getFilteredList(target, type.getOsmClass()).size();
-                if (count > 0) {
-                    ret.put(type, count);
-                }
-            }
-            return ret;
-        }
-
-        /**
-         * Pastes the tags from a homogeneous source (the selection consisting
-         * of one type of {@link OsmPrimitive}s only).
-         *
-         * Tags from a homogeneous source can be pasted to a heterogeneous target. All target primitives,
-         * regardless of their type, receive the same tags.
-         */
-        protected void pasteFromHomogeneousSource() {
-            TagCollection tc = null;
-            for (OsmPrimitiveType type : OsmPrimitiveType.dataValues()) {
-                TagCollection tc1 = getSourceTagsByType(type);
-                if (!tc1.isEmpty()) {
-                    tc = tc1;
-                }
-            }
-            if (tc == null)
-                // no tags found to paste. Abort.
-                return;
-
-            if (!tc.isApplicableToPrimitive()) {
-                PasteTagsConflictResolverDialog dialog = new PasteTagsConflictResolverDialog(Main.parent);
-                dialog.populate(tc, getSourceStatistics(), getTargetStatistics());
-                dialog.setVisible(true);
-                if (dialog.isCanceled())
-                    return;
-                buildTags(dialog.getResolution());
-            } else {
-                // no conflicts in the source tags to resolve. Just apply the tags to the target primitives
-                buildTags(tc);
-            }
-        }
-
-        /**
-         * Replies true if there is at least one primitive of type <code>type</code>
-         * is in the target collection
-         *
-         * @param type  the type to look for
-         * @return true if there is at least one primitive of type <code>type</code> in the collection
-         * <code>selection</code>
-         */
-        protected boolean hasTargetPrimitives(Class<? extends OsmPrimitive> type) {
-            return !OsmPrimitive.getFilteredList(target, type).isEmpty();
-        }
-
-        /**
-         * Replies true if this a heterogeneous source can be pasted without conflict to targets
-         *
-         * @return true if this a heterogeneous source can be pasted without conflicts to targets
-         */
-        protected boolean canPasteFromHeterogeneousSourceWithoutConflict() {
-            for (OsmPrimitiveType type : OsmPrimitiveType.dataValues()) {
-                if (hasTargetPrimitives(type.getOsmClass())) {
-                    TagCollection tc = TagCollection.unionOfAllPrimitives(getSourcePrimitivesByType(type));
-                    if (!tc.isEmpty() && !tc.isApplicableToPrimitive())
-                        return false;
-                }
-            }
-            return true;
-        }
-
-        /**
-         * Pastes the tags in the current selection of the paste buffer to a set of target primitives.
-         */
-        protected void pasteFromHeterogeneousSource() {
-            if (canPasteFromHeterogeneousSourceWithoutConflict()) {
-                for (OsmPrimitiveType type : OsmPrimitiveType.dataValues()) {
-                    if (hasSourceTagsByType(type) && hasTargetPrimitives(type.getOsmClass())) {
-                        buildTags(getSourceTagsByType(type));
-                    }
-                }
-            } else {
-                PasteTagsConflictResolverDialog dialog = new PasteTagsConflictResolverDialog(Main.parent);
-                dialog.populate(
-                        getSourceTagsByType(OsmPrimitiveType.NODE),
-                        getSourceTagsByType(OsmPrimitiveType.WAY),
-                        getSourceTagsByType(OsmPrimitiveType.RELATION),
-                        getSourceStatistics(),
-                        getTargetStatistics()
-                );
-                dialog.setVisible(true);
-                if (dialog.isCanceled())
-                    return;
-                for (OsmPrimitiveType type : OsmPrimitiveType.dataValues()) {
-                    if (hasSourceTagsByType(type) && hasTargetPrimitives(type.getOsmClass())) {
-                        buildTags(dialog.getResolution(type));
-                    }
-                }
-            }
-        }
-
-        /**
-         * Performs the paste operation.
-         * @return list of tags
-         */
-        public List<Tag> execute() {
-            tags.clear();
-            if (isHeterogeneousSource()) {
-                pasteFromHeterogeneousSource();
-            } else {
-                pasteFromHomogeneousSource();
-            }
-            return tags;
-        }
-
-    }
-
     @Override
     public void actionPerformed(ActionEvent e) {
         Collection<OsmPrimitive> selection = getLayerManager().getEditDataSet().getSelected();
@@ -263,49 +47,6 @@ public final class PasteTagsAction extends JosmAction {
         transferHandler.pasteTags(selection);
     }
 
-    /**
-     * Paste tags from arbitrary text, not using JOSM buffer
-     * @param selection selected primitives
-     * @param text text containing tags
-     * @return true if action was successful
-     * @see TextTagParser#readTagsFromText
-     */
-    public static boolean pasteTagsFromText(Collection<OsmPrimitive> selection, String text) {
-        Map<String, String> tags = TextTagParser.readTagsFromText(text);
-        if (tags == null || tags.isEmpty()) {
-            TextTagParser.showBadBufferMessage(help);
-            return false;
-        }
-        if (!TextTagParser.validateTags(tags)) return false;
-
-        List<Command> commands = new ArrayList<>(tags.size());
-        for (Entry<String, String> entry: tags.entrySet()) {
-            String v = entry.getValue();
-            commands.add(new ChangePropertyCommand(selection, entry.getKey(), "".equals(v) ? null : v));
-        }
-        commitCommands(selection, commands);
-        return !commands.isEmpty();
-    }
-
-    /**
-     * Create and execute SequenceCommand with descriptive title
-     * @param selection selected primitives
-     * @param commands the commands to perform in a sequential command
-     */
-    private static void commitCommands(Collection<OsmPrimitive> selection, List<Command> commands) {
-        if (!commands.isEmpty()) {
-            String title1 = trn("Pasting {0} tag", "Pasting {0} tags", commands.size(), commands.size());
-            String title2 = trn("to {0} object", "to {0} objects", selection.size(), selection.size());
-            @I18n.QuirkyPluralString
-            final String title = title1 + ' ' + title2;
-            Main.main.undoRedo.add(
-                    new SequenceCommand(
-                            title,
-                            commands
-                    ));
-        }
-    }
-
     @Override
     protected void updateEnabledState() {
         DataSet ds = getLayerManager().getEditDataSet();
diff --git a/src/org/openstreetmap/josm/data/osm/Tag.java b/src/org/openstreetmap/josm/data/osm/Tag.java
index 826d8e8..f02cff5 100644
--- a/src/org/openstreetmap/josm/data/osm/Tag.java
+++ b/src/org/openstreetmap/josm/data/osm/Tag.java
@@ -1,6 +1,7 @@
 // License: GPL. For details, see LICENSE file.
 package org.openstreetmap.josm.data.osm;
 
+import java.io.Serializable;
 import java.util.Collection;
 import java.util.Collections;
 import java.util.Map;
@@ -16,7 +17,9 @@ import org.openstreetmap.josm.tools.Utils;
  * It implements the {@link Tagged} interface. However, since instances of this class are immutable,
  * the modifying methods throw an {@link UnsupportedOperationException}.
  */
-public class Tag implements Tagged, Entry<String, String> {
+public class Tag implements Tagged, Entry<String, String>, Serializable {
+
+    private static final long serialVersionUID = 1;
 
     private final String key;
     private final String value;
diff --git a/src/org/openstreetmap/josm/gui/conflict/tags/PasteTagsConflictResolverDialog.java b/src/org/openstreetmap/josm/gui/conflict/tags/PasteTagsConflictResolverDialog.java
index af392dc..f85f934 100644
--- a/src/org/openstreetmap/josm/gui/conflict/tags/PasteTagsConflictResolverDialog.java
+++ b/src/org/openstreetmap/josm/gui/conflict/tags/PasteTagsConflictResolverDialog.java
@@ -289,17 +289,13 @@ public class PasteTagsConflictResolverDialog extends JDialog implements Property
             setVisible(false);
         }
 
-        protected void updateEnabledState() {
+        void updateEnabledState() {
             if (mode == null) {
                 setEnabled(false);
             } else if (mode.equals(Mode.RESOLVING_ONE_TAGCOLLECTION_ONLY)) {
                 setEnabled(allPrimitivesResolver.getModel().isResolvedCompletely());
             } else {
-                boolean enabled = true;
-                for (TagConflictResolver val: resolvers.values()) {
-                    enabled &= val.getModel().isResolvedCompletely();
-                }
-                setEnabled(enabled);
+                setEnabled(resolvers.values().stream().allMatch(val -> val.getModel().isResolvedCompletely()));
             }
         }
 
@@ -345,7 +341,7 @@ public class PasteTagsConflictResolverDialog extends JDialog implements Property
                 TagConflictResolver resolver = (TagConflictResolver) tpResolvers.getComponentAt(i);
                 if (model == resolver.getModel()) {
                     tpResolvers.setIconAt(i,
-                            (Boolean) evt.getNewValue() ? iconResolved : iconUnresolved
+                            (Integer) evt.getNewValue() == 0 ? iconResolved : iconUnresolved
 
                     );
                 }
diff --git a/src/org/openstreetmap/josm/gui/conflict/tags/TagConflictResolverModel.java b/src/org/openstreetmap/josm/gui/conflict/tags/TagConflictResolverModel.java
index a085c4f..a347d4c 100644
--- a/src/org/openstreetmap/josm/gui/conflict/tags/TagConflictResolverModel.java
+++ b/src/org/openstreetmap/josm/gui/conflict/tags/TagConflictResolverModel.java
@@ -21,7 +21,7 @@ public class TagConflictResolverModel extends DefaultTableModel {
 
     private transient TagCollection tags;
     private List<String> displayedKeys;
-    private Set<String> keysWithConflicts;
+    private Set<String> keysWithConflicts = new HashSet<>();
     private transient Map<String, MultiValueResolutionDecision> decisions;
     private int numConflicts;
     private final PropertyChangeSupport support;
@@ -53,13 +53,7 @@ public class TagConflictResolverModel extends DefaultTableModel {
     }
 
     protected void refreshNumConflicts() {
-        int count = 0;
-        for (MultiValueResolutionDecision d : decisions.values()) {
-            if (!d.isDecided()) {
-                count++;
-            }
-        }
-        setNumConflicts(count);
+        setNumConflicts((int) decisions.values().stream().filter(d -> !d.isDecided()).count());
     }
 
     protected void sort() {
@@ -121,7 +115,9 @@ public class TagConflictResolverModel extends DefaultTableModel {
         CheckParameterUtil.ensureParameterNotNull(tags, "tags");
         this.tags = tags;
         displayedKeys = new ArrayList<>();
-        this.keysWithConflicts = keysWithConflicts == null ? new HashSet<>() : keysWithConflicts;
+        if (keysWithConflicts != null) {
+            this.keysWithConflicts.addAll(keysWithConflicts);
+        }
         decisions = new HashMap<>();
         rebuild();
     }
@@ -182,13 +178,21 @@ public class TagConflictResolverModel extends DefaultTableModel {
      * @return true if each {@link MultiValueResolutionDecision} is decided; false otherwise
      */
     public boolean isResolvedCompletely() {
-        return numConflicts == 0 && keysWithConflicts != null && keysWithConflicts.isEmpty();
+        return numConflicts == 0;
     }
 
+    /**
+     * Gets the number of reamining conflicts.
+     * @return The number
+     */
     public int getNumConflicts() {
         return numConflicts;
     }
 
+    /**
+     * Gets the number of decisions the user can take
+     * @return The number of decisions
+     */
     public int getNumDecisions() {
         return decisions == null ? 0 : decisions.size();
     }
diff --git a/src/org/openstreetmap/josm/gui/datatransfer/OsmTransferHandler.java b/src/org/openstreetmap/josm/gui/datatransfer/OsmTransferHandler.java
index 5076f86..5001c48 100644
--- a/src/org/openstreetmap/josm/gui/datatransfer/OsmTransferHandler.java
+++ b/src/org/openstreetmap/josm/gui/datatransfer/OsmTransferHandler.java
@@ -16,6 +16,7 @@ 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.PrimitiveTagTransferPaster;
 import org.openstreetmap.josm.gui.datatransfer.importers.TagTransferPaster;
 import org.openstreetmap.josm.gui.datatransfer.importers.TextTagPaster;
 import org.openstreetmap.josm.gui.layer.OsmDataLayer;
@@ -29,6 +30,7 @@ public class OsmTransferHandler extends TransferHandler {
 
     private static final Collection<AbstractOsmDataPaster> SUPPORTED = Arrays.asList(
             new FilePaster(), new PrimitiveDataPaster(),
+            new PrimitiveTagTransferPaster(),
             new TagTransferPaster(), new TextTagPaster());
 
     @Override
diff --git a/src/org/openstreetmap/josm/gui/datatransfer/PrimitiveTransferable.java b/src/org/openstreetmap/josm/gui/datatransfer/PrimitiveTransferable.java
index 140276e..8d6c9a0 100644
--- a/src/org/openstreetmap/josm/gui/datatransfer/PrimitiveTransferable.java
+++ b/src/org/openstreetmap/josm/gui/datatransfer/PrimitiveTransferable.java
@@ -11,6 +11,7 @@ import java.util.List;
 import org.openstreetmap.josm.data.osm.OsmPrimitiveType;
 import org.openstreetmap.josm.data.osm.PrimitiveData;
 import org.openstreetmap.josm.gui.datatransfer.data.OsmLayerTransferData;
+import org.openstreetmap.josm.gui.datatransfer.data.PrimitiveTagTransferData;
 import org.openstreetmap.josm.gui.datatransfer.data.PrimitiveTransferData;
 import org.openstreetmap.josm.gui.datatransfer.data.TagTransferData;
 import org.openstreetmap.josm.gui.layer.OsmDataLayer;
@@ -74,6 +75,8 @@ public class PrimitiveTransferable implements Transferable {
             return getStringData();
         } else if (PrimitiveTransferData.DATA_FLAVOR.equals(flavor)) {
             return primitives;
+        } else if (PrimitiveTagTransferData.FLAVOR.equals(flavor)) {
+            return new PrimitiveTagTransferData(primitives);
         } else if (TagTransferData.FLAVOR.equals(flavor)) {
             return new TagTransferData(primitives.getDirectlyAdded());
         } else if (sourceLayer != null && OsmLayerTransferData.FLAVORS.contains(flavor)) {
diff --git a/src/org/openstreetmap/josm/gui/datatransfer/data/PrimitiveTagTransferData.java b/src/org/openstreetmap/josm/gui/datatransfer/data/PrimitiveTagTransferData.java
new file mode 100644
index 0000000..37ef416
--- /dev/null
+++ b/src/org/openstreetmap/josm/gui/datatransfer/data/PrimitiveTagTransferData.java
@@ -0,0 +1,90 @@
+// 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.EnumMap;
+import java.util.Map;
+
+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.data.osm.TagCollection;
+
+/**
+ * This is a variant of {@link TagTransferData} that holds tags that were copied from a collection of primitives.
+ * @author Michael Zangl
+ * @since xxx
+ */
+public class PrimitiveTagTransferData implements Serializable {
+
+    private static final long serialVersionUID = 1;
+
+    /**
+     * This is a data flavor added
+     */
+    public static final DataFlavor FLAVOR = new DataFlavor(TagTransferData.class, "OSM Primitive Tags");
+
+    private final EnumMap<OsmPrimitiveType, TagCollection> tags = new EnumMap<>(OsmPrimitiveType.class);
+    private final EnumMap<OsmPrimitiveType, Integer> counts = new EnumMap<>(OsmPrimitiveType.class);
+
+    /**
+     * Create a new {@link PrimitiveTagTransferData}
+     * @param source The primitives to initialize this object with.
+     */
+    public PrimitiveTagTransferData(Collection<? extends PrimitiveData> source) {
+        for (OsmPrimitiveType type : OsmPrimitiveType.dataValues()) {
+            tags.put(type, new TagCollection());
+        }
+
+        for (PrimitiveData primitive : source) {
+            tags.get(primitive.getType()).add(TagCollection.from(primitive));
+            counts.merge(primitive.getType(), 1, (a, b) -> a + b);
+        }
+    }
+
+    /**
+     * Create a new {@link PrimitiveTagTransferData}
+     * @param data The primitives to initialize this object with.
+     */
+    public PrimitiveTagTransferData(PrimitiveTransferData data) {
+        this(data.getDirectlyAdded());
+    }
+
+    /**
+     * Determines if the source for tag pasting is heterogeneous, i.e. if it doesn't consist of
+     * {@link OsmPrimitive}s of exactly one type
+     * @return true if the source for tag pasting is heterogeneous
+     */
+    public boolean isHeterogeneousSource() {
+        return counts.size() > 1;
+    }
+
+    /**
+     * Gets the tags used for this primitive type.
+     * @param type The primitive type
+     * @return The tags as collection. Empty if no such type was copied
+     */
+    public TagCollection getForPrimitives(OsmPrimitiveType type) {
+        return tags.get(type);
+    }
+
+    /**
+     * Gets the number of source primitives for the given type.
+     * @param type The type
+     * @return The number of source primitives of that type
+     */
+    public int getSourcePrimitiveCount(OsmPrimitiveType type) {
+        return counts.getOrDefault(type, 0);
+    }
+
+    /**
+     * Gets the statistics of the source primitive counts. May contain no entries for unused types.
+     * @return The statistics as map
+     */
+    public Map<OsmPrimitiveType, Integer> getStatistics() {
+        return Collections.unmodifiableMap(counts);
+    }
+}
diff --git a/src/org/openstreetmap/josm/gui/datatransfer/data/TagTransferData.java b/src/org/openstreetmap/josm/gui/datatransfer/data/TagTransferData.java
index 52040d2..6bc5e58 100644
--- a/src/org/openstreetmap/josm/gui/datatransfer/data/TagTransferData.java
+++ b/src/org/openstreetmap/josm/gui/datatransfer/data/TagTransferData.java
@@ -13,7 +13,7 @@ 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.
+ * It contains all tags contained in the selection that was copied. For conflicting tags, any of the values may be used.
  * @author Michael Zangl
  * @since 10604
  */
diff --git a/src/org/openstreetmap/josm/gui/datatransfer/importers/AbstractTagPaster.java b/src/org/openstreetmap/josm/gui/datatransfer/importers/AbstractTagPaster.java
index 12f4a96..8c5da7f 100644
--- a/src/org/openstreetmap/josm/gui/datatransfer/importers/AbstractTagPaster.java
+++ b/src/org/openstreetmap/josm/gui/datatransfer/importers/AbstractTagPaster.java
@@ -1,19 +1,26 @@
 // License: GPL. For details, see LICENSE file.
 package org.openstreetmap.josm.gui.datatransfer.importers;
 
+import static org.openstreetmap.josm.tools.I18n.trn;
+
 import java.awt.datatransfer.DataFlavor;
 import java.awt.datatransfer.UnsupportedFlavorException;
 import java.io.IOException;
 import java.util.Collection;
+import java.util.Collections;
+import java.util.List;
 import java.util.Map;
 
 import javax.swing.TransferHandler.TransferSupport;
 
 import org.openstreetmap.josm.Main;
 import org.openstreetmap.josm.command.ChangePropertyCommand;
+import org.openstreetmap.josm.command.Command;
+import org.openstreetmap.josm.command.SequenceCommand;
 import org.openstreetmap.josm.data.coor.EastNorth;
 import org.openstreetmap.josm.data.osm.OsmPrimitive;
 import org.openstreetmap.josm.gui.layer.OsmDataLayer;
+import org.openstreetmap.josm.tools.I18n;
 
 /**
  * This transfer support allows us to transfer tags to the selected primitives
@@ -41,11 +48,29 @@ public abstract class AbstractTagPaster extends AbstractOsmDataPaster {
     public boolean importTagsOn(TransferSupport support, Collection<? extends OsmPrimitive> selection)
             throws UnsupportedFlavorException, IOException {
         ChangePropertyCommand command = new ChangePropertyCommand(selection, getTags(support));
-        Main.main.undoRedo.add(command);
+        commitCommands(selection, Collections.singletonList(command));
         return true;
     }
 
     /**
+     * Create and execute SequenceCommand with descriptive title
+     * @param selection selected primitives
+     * @param commands the commands to perform in a sequential command
+     */
+    protected static void commitCommands(Collection<? extends OsmPrimitive> selection, List<Command> commands) {
+        if (!commands.isEmpty()) {
+            String title1 = trn("Pasting {0} tag", "Pasting {0} tags", commands.size(), commands.size());
+            String title2 = trn("to {0} object", "to {0} objects", selection.size(), selection.size());
+            @I18n.QuirkyPluralString
+            final String title = title1 + ' ' + title2;
+            Main.main.undoRedo.add(
+                    new SequenceCommand(
+                            title,
+                            commands
+                    ));
+        }
+    }
+    /**
      * Gets the tags that should be pasted.
      * @param support The TransferSupport to get the tags from.
      * @return The tags
diff --git a/src/org/openstreetmap/josm/gui/datatransfer/importers/PrimitiveTagTransferPaster.java b/src/org/openstreetmap/josm/gui/datatransfer/importers/PrimitiveTagTransferPaster.java
new file mode 100644
index 0000000..0a09169
--- /dev/null
+++ b/src/org/openstreetmap/josm/gui/datatransfer/importers/PrimitiveTagTransferPaster.java
@@ -0,0 +1,206 @@
+// License: GPL. For details, see LICENSE file.
+package org.openstreetmap.josm.gui.datatransfer.importers;
+
+import static org.openstreetmap.josm.gui.datatransfer.importers.AbstractTagPaster.commitCommands;
+
+import java.awt.datatransfer.UnsupportedFlavorException;
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.EnumMap;
+import java.util.List;
+import java.util.Map;
+
+import javax.swing.TransferHandler.TransferSupport;
+
+import org.openstreetmap.josm.Main;
+import org.openstreetmap.josm.command.ChangePropertyCommand;
+import org.openstreetmap.josm.command.Command;
+import org.openstreetmap.josm.data.osm.IPrimitive;
+import org.openstreetmap.josm.data.osm.Node;
+import org.openstreetmap.josm.data.osm.OsmPrimitive;
+import org.openstreetmap.josm.data.osm.OsmPrimitiveType;
+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.gui.conflict.tags.PasteTagsConflictResolverDialog;
+import org.openstreetmap.josm.gui.datatransfer.data.PrimitiveTagTransferData;
+
+/**
+ * This class helps pasting tags form other primitives. It handles resolving conflicts.
+ * @author Michael Zangl
+ * @since xxx
+ */
+public class PrimitiveTagTransferPaster extends AbstractTagPaster {
+    /**
+     * Create a new {@link PrimitiveTagTransferPaster}
+     */
+    public PrimitiveTagTransferPaster() {
+        super(PrimitiveTagTransferData.FLAVOR);
+    }
+
+    @Override
+    public boolean importTagsOn(TransferSupport support, Collection<? extends OsmPrimitive> selection)
+            throws UnsupportedFlavorException, IOException {
+        PrimitiveTagTransferData data = (PrimitiveTagTransferData) support.getTransferable().getTransferData(df);
+
+        TagPasteSupport tagPaster = new TagPasteSupport(data, 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;
+    }
+
+    @Override
+    protected Map<String, String> getTags(TransferSupport support) throws UnsupportedFlavorException, IOException {
+        PrimitiveTagTransferData data = (PrimitiveTagTransferData) support.getTransferable().getTransferData(df);
+
+        TagPasteSupport tagPaster = new TagPasteSupport(data, Arrays.asList(new Node()));
+        return new TagMap(tagPaster.execute());
+    }
+
+    private static class TagPasteSupport {
+        private final PrimitiveTagTransferData data;
+        private final Collection<? extends IPrimitive> selection;
+        private final List<Tag> tags = new ArrayList<>();
+
+        /**
+         * Constructs a new {@code TagPasteSupport}.
+         * @param data source tags to paste
+         * @param selection target primitives
+         */
+        TagPasteSupport(PrimitiveTagTransferData data, Collection<? extends IPrimitive> selection) {
+            super();
+            this.data = data;
+            this.selection = selection;
+        }
+
+        /**
+         * Pastes the tags from a homogeneous source (the selection consisting
+         * of one type of {@link OsmPrimitive}s only).
+         *
+         * Tags from a homogeneous source can be pasted to a heterogeneous target. All target primitives,
+         * regardless of their type, receive the same tags.
+         */
+        protected void pasteFromHomogeneousSource() {
+            TagCollection tc = null;
+            for (OsmPrimitiveType type : OsmPrimitiveType.dataValues()) {
+                TagCollection tc1 = data.getForPrimitives(type);
+                if (!tc1.isEmpty()) {
+                    tc = tc1;
+                }
+            }
+            if (tc == null)
+                // no tags found to paste. Abort.
+                return;
+
+            if (!tc.isApplicableToPrimitive()) {
+                PasteTagsConflictResolverDialog dialog = new PasteTagsConflictResolverDialog(Main.parent);
+                dialog.populate(tc, data.getStatistics(), getTargetStatistics());
+                dialog.setVisible(true);
+                if (dialog.isCanceled())
+                    return;
+                buildTags(dialog.getResolution());
+            } else {
+                // no conflicts in the source tags to resolve. Just apply the tags to the target primitives
+                buildTags(tc);
+            }
+        }
+
+        /**
+         * Replies true if this a heterogeneous source can be pasted without conflict to targets
+         *
+         * @return true if this a heterogeneous source can be pasted without conflicts to targets
+         */
+        protected boolean canPasteFromHeterogeneousSourceWithoutConflict() {
+            for (OsmPrimitiveType type : OsmPrimitiveType.dataValues()) {
+                if (hasTargetPrimitives(type)) {
+                    TagCollection tc = data.getForPrimitives(type);
+                    if (!tc.isEmpty() && !tc.isApplicableToPrimitive())
+                        return false;
+                }
+            }
+            return true;
+        }
+
+        /**
+         * Pastes the tags in the current selection of the paste buffer to a set of target primitives.
+         */
+        protected void pasteFromHeterogeneousSource() {
+            if (canPasteFromHeterogeneousSourceWithoutConflict()) {
+                for (OsmPrimitiveType type : OsmPrimitiveType.dataValues()) {
+                    if (!data.getForPrimitives(type).isEmpty() && hasTargetPrimitives(type)) {
+                        buildTags(data.getForPrimitives(type));
+                    }
+                }
+            } else {
+                PasteTagsConflictResolverDialog dialog = new PasteTagsConflictResolverDialog(Main.parent);
+                dialog.populate(
+                        data.getForPrimitives(OsmPrimitiveType.NODE),
+                        data.getForPrimitives(OsmPrimitiveType.WAY),
+                        data.getForPrimitives(OsmPrimitiveType.RELATION),
+                        data.getStatistics(),
+                        getTargetStatistics()
+                );
+                dialog.setVisible(true);
+                if (dialog.isCanceled())
+                    return;
+                for (OsmPrimitiveType type : OsmPrimitiveType.dataValues()) {
+                    if (!data.getForPrimitives(type).isEmpty() && hasTargetPrimitives(type)) {
+                        buildTags(dialog.getResolution(type));
+                    }
+                }
+            }
+        }
+
+        protected Map<OsmPrimitiveType, Integer> getTargetStatistics() {
+            Map<OsmPrimitiveType, Integer> ret = new EnumMap<>(OsmPrimitiveType.class);
+            for (OsmPrimitiveType type: OsmPrimitiveType.dataValues()) {
+                int count = (int) selection.stream().filter(p -> type == p.getType()).count();
+                if (count > 0) {
+                    ret.put(type, count);
+                }
+            }
+            return ret;
+        }
+        /**
+         * Replies true if there is at least one primitive of type <code>type</code>
+         * is in the target collection
+         *
+         * @param type  the type to look for
+         * @return true if there is at least one primitive of type <code>type</code> in the collection
+         * <code>selection</code>
+         */
+        protected boolean hasTargetPrimitives(OsmPrimitiveType type) {
+            return selection.stream().anyMatch(p -> type == p.getType());
+        }
+
+        protected void buildTags(TagCollection tc) {
+            for (String key : tc.getKeys()) {
+                tags.add(new Tag(key, tc.getValues(key).iterator().next()));
+            }
+        }
+
+        /**
+         * Performs the paste operation.
+         * @return list of tags
+         */
+        public List<Tag> execute() {
+            tags.clear();
+            if (data.isHeterogeneousSource()) {
+                pasteFromHeterogeneousSource();
+            } else {
+                pasteFromHomogeneousSource();
+            }
+            return tags;
+        }
+
+        @Override
+        public String toString() {
+            return "PasteSupport [data=" + data + ", selection=" + selection + "]";
+        }
+    }
+}
diff --git a/src/org/openstreetmap/josm/gui/datatransfer/importers/TextTagPaster.java b/src/org/openstreetmap/josm/gui/datatransfer/importers/TextTagPaster.java
index 5b63ec7..404cc32 100644
--- a/src/org/openstreetmap/josm/gui/datatransfer/importers/TextTagPaster.java
+++ b/src/org/openstreetmap/josm/gui/datatransfer/importers/TextTagPaster.java
@@ -1,6 +1,8 @@
 // License: GPL. For details, see LICENSE file.
 package org.openstreetmap.josm.gui.datatransfer.importers;
 
+import static org.openstreetmap.josm.gui.help.HelpUtil.ht;
+
 import java.awt.datatransfer.DataFlavor;
 import java.awt.datatransfer.UnsupportedFlavorException;
 import java.io.IOException;
@@ -17,6 +19,7 @@ import org.openstreetmap.josm.tools.TextTagParser;
  * @since 10604
  */
 public final class TextTagPaster extends AbstractTagPaster {
+    private static final String help = ht("/Action/PasteTags");
 
     /**
      * Create a new {@link TextTagPaster}
@@ -28,15 +31,32 @@ public final class TextTagPaster extends AbstractTagPaster {
     @Override
     public boolean supports(TransferSupport support) {
         try {
-            return super.supports(support) && getTags(support) != null;
+            return super.supports(support) && containsValidTags(support);
         } catch (UnsupportedFlavorException | IOException e) {
             Main.warn(e);
             return false;
         }
     }
 
+    private boolean containsValidTags(TransferSupport support) throws UnsupportedFlavorException, IOException {
+        Map<String, String> tags = getTagsImpl(support);
+        return tags != null && !tags.isEmpty();
+    }
+
     @Override
     protected Map<String, String> getTags(TransferSupport support) throws UnsupportedFlavorException, IOException {
+        Map<String, String> tags = getTagsImpl(support);
+        if (tags == null || tags.isEmpty()) {
+            TextTagParser.showBadBufferMessage(help);
+            throw new IOException("Invalid tags to paste.");
+        }
+        if (!TextTagParser.validateTags(tags)) {
+            throw new IOException("Tags to paste are not valid.");
+        }
+        return tags;
+    }
+
+    private Map<String, String> getTagsImpl(TransferSupport support) throws UnsupportedFlavorException, IOException {
         return TextTagParser.readTagsFromText((String) support.getTransferable().getTransferData(df));
     }
 }
diff --git a/test/unit/org/openstreetmap/josm/actions/PasteTagsActionTest.java b/test/unit/org/openstreetmap/josm/actions/PasteTagsActionTest.java
deleted file mode 100644
index 0763efa..0000000
--- a/test/unit/org/openstreetmap/josm/actions/PasteTagsActionTest.java
+++ /dev/null
@@ -1,55 +0,0 @@
-// License: GPL. For details, see LICENSE file.
-package org.openstreetmap.josm.actions;
-
-import static org.junit.Assert.assertFalse;
-import static org.junit.Assert.assertTrue;
-
-import java.util.Arrays;
-
-import org.junit.BeforeClass;
-import org.junit.Test;
-import org.openstreetmap.josm.JOSMFixture;
-import org.openstreetmap.josm.actions.PasteTagsAction.TagPaster;
-import org.openstreetmap.josm.data.osm.NodeData;
-import org.openstreetmap.josm.data.osm.PrimitiveData;
-import org.openstreetmap.josm.data.osm.RelationData;
-import org.openstreetmap.josm.data.osm.WayData;
-
-/**
- * Unit tests for class {@link PasteTagsAction}.
- */
-public class PasteTagsActionTest {
-
-    /**
-     * Setup test.
-     */
-    @BeforeClass
-    public static void setUpBeforeClass() {
-        JOSMFixture.createUnitTestFixture().init();
-    }
-
-    private static boolean isHeterogeneousSource(PrimitiveData... t) {
-        return new TagPaster(Arrays.asList(t), null).isHeterogeneousSource();
-    }
-
-    /**
-     * Unit test of {@link TagPaster#isHeterogeneousSource}.
-     */
-    @Test
-    public void testTagPasterIsHeterogeneousSource() {
-        // 0 item
-        assertFalse(isHeterogeneousSource());
-        // 1 item
-        assertFalse(isHeterogeneousSource(new NodeData()));
-        assertFalse(isHeterogeneousSource(new WayData()));
-        assertFalse(isHeterogeneousSource(new RelationData()));
-        // 2 items of same type
-        assertFalse(isHeterogeneousSource(new NodeData(), new NodeData()));
-        assertFalse(isHeterogeneousSource(new WayData(), new WayData()));
-        assertFalse(isHeterogeneousSource(new RelationData(), new RelationData()));
-        // 2 items of different type
-        assertTrue(isHeterogeneousSource(new NodeData(), new WayData()));
-        assertTrue(isHeterogeneousSource(new NodeData(), new RelationData()));
-        assertTrue(isHeterogeneousSource(new WayData(), new RelationData()));
-    }
-}
diff --git a/test/unit/org/openstreetmap/josm/gui/datatransfer/data/PrimitiveTagTransferDataTest.java b/test/unit/org/openstreetmap/josm/gui/datatransfer/data/PrimitiveTagTransferDataTest.java
new file mode 100644
index 0000000..8f0e10b
--- /dev/null
+++ b/test/unit/org/openstreetmap/josm/gui/datatransfer/data/PrimitiveTagTransferDataTest.java
@@ -0,0 +1,123 @@
+// License: GPL. For details, see LICENSE file.
+package org.openstreetmap.josm.gui.datatransfer.data;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
+
+import java.util.Arrays;
+import java.util.Map;
+
+import org.junit.Rule;
+import org.junit.Test;
+import org.openstreetmap.josm.data.osm.Node;
+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.TagCollection;
+import org.openstreetmap.josm.data.osm.WayData;
+import org.openstreetmap.josm.testutils.JOSMTestRules;
+
+import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
+
+/**
+ * Test {@link PrimitiveTagTransferData}
+ * @author Michael Zangl
+ * @since xxx
+ */
+public class PrimitiveTagTransferDataTest {
+    /**
+     * Prefs only required because of the dependencies of OSM primitives.
+     */
+    @Rule
+    @SuppressFBWarnings(value = "URF_UNREAD_PUBLIC_OR_PROTECTED_FIELD")
+    public JOSMTestRules test = new JOSMTestRules().preferences();
+
+
+    private static boolean isHeterogeneousSource(PrimitiveData... t) {
+        return new PrimitiveTagTransferData(Arrays.asList(t)).isHeterogeneousSource();
+    }
+
+    /**
+     * Test method for {@link org.openstreetmap.josm.gui.datatransfer.data.PrimitiveTagTransferData#PrimitiveTagTransferData(org.openstreetmap.josm.gui.datatransfer.data.PrimitiveTransferData)}.
+     */
+    @Test
+    public void testPrimitiveTagTransferDataPrimitiveTransferData() {
+        PrimitiveTagTransferData data = new PrimitiveTagTransferData(PrimitiveTransferData.getData(Arrays.asList(new Node(), new Node())));
+        assertEquals(2, data.getSourcePrimitiveCount(OsmPrimitiveType.NODE));
+        assertEquals(0, data.getSourcePrimitiveCount(OsmPrimitiveType.WAY));
+        assertEquals(0, data.getSourcePrimitiveCount(OsmPrimitiveType.RELATION));
+    }
+
+    /**
+     * Test method for {@link org.openstreetmap.josm.gui.datatransfer.data.PrimitiveTagTransferData#isHeterogeneousSource()}.
+     */
+    @Test
+    public void testIsHeterogeneousSource() {
+        // 0 item
+        assertFalse(isHeterogeneousSource());
+        // 1 item
+        assertFalse(isHeterogeneousSource(new NodeData()));
+        assertFalse(isHeterogeneousSource(new WayData()));
+        assertFalse(isHeterogeneousSource(new RelationData()));
+        // 2 items of same type
+        assertFalse(isHeterogeneousSource(new NodeData(), new NodeData()));
+        assertFalse(isHeterogeneousSource(new WayData(), new WayData()));
+        assertFalse(isHeterogeneousSource(new RelationData(), new RelationData()));
+        // 2 items of different type
+        assertTrue(isHeterogeneousSource(new NodeData(), new WayData()));
+        assertTrue(isHeterogeneousSource(new NodeData(), new RelationData()));
+        assertTrue(isHeterogeneousSource(new WayData(), new RelationData()));
+    }
+
+    /**
+     * Test method for {@link org.openstreetmap.josm.gui.datatransfer.data.PrimitiveTagTransferData#getForPrimitives(org.openstreetmap.josm.data.osm.OsmPrimitiveType)}.
+     */
+    @Test
+    public void testGetForPrimitives() {
+        PrimitiveTagTransferData data = createTestData();
+        TagCollection forNode = data.getForPrimitives(OsmPrimitiveType.NODE);
+        assertEquals(1, forNode.getKeys().size());
+        assertEquals(2, forNode.getValues("k").size());
+        TagCollection forWay = data.getForPrimitives(OsmPrimitiveType.WAY);
+        assertEquals(1, forWay.getKeys().size());
+        assertEquals(1, forWay.getValues("x").size());
+        TagCollection forRelation = data.getForPrimitives(OsmPrimitiveType.RELATION);
+        assertEquals(0, forRelation.getKeys().size());
+    }
+
+    private PrimitiveTagTransferData createTestData() {
+        NodeData nd1 = new NodeData();
+        nd1.put("k", "v");
+        NodeData nd2 = new NodeData();
+        nd2.put("k", "v2");
+        WayData way = new WayData();
+        way.put("x", "v");
+        return new PrimitiveTagTransferData(Arrays.asList(nd1, nd2, way));
+    }
+
+    /**
+     * Test method for {@link org.openstreetmap.josm.gui.datatransfer.data.PrimitiveTagTransferData#getSourcePrimitiveCount(org.openstreetmap.josm.data.osm.OsmPrimitiveType)}.
+     */
+    @Test
+    public void testGetSourcePrimitiveCount() {
+        PrimitiveTagTransferData data = createTestData();
+        assertEquals(2, data.getSourcePrimitiveCount(OsmPrimitiveType.NODE));
+        assertEquals(1, data.getSourcePrimitiveCount(OsmPrimitiveType.WAY));
+        assertEquals(0, data.getSourcePrimitiveCount(OsmPrimitiveType.RELATION));
+    }
+
+    /**
+     * Test method for {@link org.openstreetmap.josm.gui.datatransfer.data.PrimitiveTagTransferData#getStatistics()}.
+     */
+    @Test
+    public void testGetStatistics() {
+        PrimitiveTagTransferData data = createTestData();
+        Map<OsmPrimitiveType, Integer> stats = data.getStatistics();
+        assertEquals(2, (int) stats.get(OsmPrimitiveType.NODE));
+        assertEquals(1, (int) stats.get(OsmPrimitiveType.WAY));
+        assertEquals(null, stats.get(OsmPrimitiveType.RELATION));
+    }
+
+}
