class ObjectHandler implements MetadataPriorityDependenciesHandler<Object>, MetadataMergeHandler<Object> {

    @Override
    public void merge(final Object existing, final Object incoming, final ImportType importType, final Map<Object, Object> incomingToExisting) {
		if (existing == null) {
			//Replace incoming object's fields with existing objects
			visitor.visitFields(incoming, false, new ObjectVisitor.FieldVisitor() {
				
				@Override
				public void visit(String fieldName, Class<?> type, Class<?> definedIn, Object incomingField) {
					if (incomingField instanceof OpenmrsObject) {
						Object existing = incomingToExisting.get(incomingField);
						if (existing != null) {
							visitor.writeField(incoming, fieldName, existing, definedIn);
						}
					} else if (incomingField instanceof Collection) {
						@SuppressWarnings("unchecked")
						Collection<Object> collection = (Collection<Object>) incomingField;
						Iterator<Object> it = collection.iterator();
						
						Collection<Object> replacements = new ArrayList<Object>();
						while (it.hasNext()) {
							incomingField = it.next();
							Object existing = incomingToExisting.get(incomingField);
							if (existing != null) {
								replacements.add(existing);
								it.remove();
							}
						}
						collection.addAll(replacements);
					}
				}
			});
		} else {
			if (importType.isPreferTheirs() || importType.isPreferMine() || importType.isOverwriteMine()) {
				//Copy properties from the incoming object to the existing object				
				Integer id = Handler.getId(existing);

				visitor.visitFields(incoming, false, new ObjectVisitor.FieldVisitor() {

					@Override
					public void visit(String fieldName, Class<?> type, Class<?> definedIn, Object incomingField) {
						if (Collection.class.isAssignableFrom(type)) {
							//If the collection field is null then do nothing
							if (incomingField == null) {
								return;
							}

							Object existingField = visitor.readField(existing, fieldName, definedIn);
							if (existingField instanceof Collection) {
								@SuppressWarnings("unchecked")
								Collection<Object> existingCollection = (Collection<Object>) existingField;
								@SuppressWarnings("unchecked")
								Collection<Object> incomingCollection = (Collection<Object>) incomingField;

								if (importType.isOverwriteMine()) {
									//We'll be skipping incoming elements if they exist.
									List<Object> incomingDiffCollection = new LinkedList<Object>(incomingCollection);

									Iterator<Object> existingCollectionIt = existingCollection.iterator();
									while(existingCollectionIt.hasNext()) {
										Object existingElement = existingCollectionIt.next();

										// note that modifications to collection members will be applied when handling the member itself as an import item
										boolean existingMissing = findAndRemoveMatchingElementByUuid(incomingDiffCollection, existingElement) == null;

										if (existingMissing) {
											if (existingElement instanceof ConceptName) {
												//ConceptNames must not be purged, but voided. See META-324.
												((ConceptName) existingElement).setVoided(true);
											} else {
												existingCollectionIt.remove();
											}
										}
									}

									//Let's add completely new incoming elements.
									for (Object incomingElement : incomingDiffCollection) {
                                        Object mappedToExisting = incomingToExisting.get(incomingElement);
                                        if (mappedToExisting != null) {
                                            existingCollection.add(mappedToExisting);
                                        } else {
                                            existingCollection.add(incomingElement);
                                        }
                                    }
								} else {
									for (Object incomingElement : incomingCollection) {
										Object existing = incomingToExisting.get(incomingElement);

										if (existing == null) {
											boolean incomingMissing = true;
											for (Object existingElement : existingCollection) {
												if (comparisonEngine.equal(incomingElement, existingElement, incomingToExisting)) {
													incomingMissing = false;
													break;
												}
											}

											if (incomingMissing) {
												existingCollection.add(incomingElement);
											}
										}
									}
								}
							}
						} else if (!importType.isPreferMine()) {
							if (incomingField instanceof OpenmrsObject) {
								Object existingField = incomingToExisting.get(incomingField);
								if (existingField != null) {
									visitor.writeField(existing, fieldName, existingField, definedIn);
								} else {
									visitor.writeField(existing, fieldName, incomingField, definedIn);
								}
							} else {
								if (!fieldName.equals("uuid") && !(incomingField instanceof User)) {
									visitor.writeField(existing, fieldName, incomingField, definedIn);
								}
							}
						}
					}

					private Object findAndRemoveMatchingElementByUuid(List<Object> incomingDiffCollection, Object existingElement) {
						Iterator<Object> incomingDiffCollectionIt = incomingDiffCollection.iterator();
						while (incomingDiffCollectionIt.hasNext()) {
							Object incomingElement = incomingDiffCollectionIt.next();
							// if this was mapped to another element, look at that one instead
							if (incomingToExisting.get(incomingElement) != null) {
								incomingElement = incomingToExisting.get(incomingElement);
							}
							if (equalUuids(existingElement, incomingElement)) {
								incomingDiffCollectionIt.remove();
								return incomingElement;
							}
						}
						return null;
					}

					private boolean equalUuids(Object a, Object b) {
						try {
							return ((OpenmrsObject) a).getUuid().equals(((OpenmrsObject) b).getUuid());
						} catch (Exception ex) {
							return false;
						}
					}
				});
				
				if (id != null) {
					Handler.setId(existing, id);
				}
			}
		}
    }

}

class ObjectHandlerTest {

    @Test
    public void testMerge() throws Exception {

//        MetadataSharing mds = Mockito.mock(MetadataSharing.class);
//
//        PowerMockito.mockStatic(MetadataSharing.class);
//        when(MetadataSharing.getInstance()).thenReturn(mds);

        PowerMockito.mockStatic(Handler.class, new Answer() {
            @Override
            public Object answer(InvocationOnMock invocation) throws Throwable {
                return null;
            }
        });

        Privilege p1 = buildPrivilege("Privilege 1", "A privilege");
        Privilege p2 = buildPrivilege("Privilege 2", "A privilege");
        Privilege p3 = buildPrivilege("Privilege 3", "A privilege");
        Privilege p4 = buildPrivilege("Privilege 4", "A privilege");

        Role existing = new Role();
        existing.addPrivilege(p2);
        existing.addPrivilege(p3);

        Role incoming = new Role();
        incoming.addPrivilege(p1);
        incoming.addPrivilege(p2);
        incoming.addPrivilege(p3);
        incoming.addPrivilege(p4);

        Map<Object, Object> incomingToExisting = new HashMap<Object, Object>();
        incomingToExisting.put(p1, p1);
        incomingToExisting.put(p2, p2);
        incomingToExisting.put(p3, p3);
        incomingToExisting.put(p4, p4);

        ObjectHandler objectHandler = new ObjectHandler(new OpenmrsObjectVisitor(), new ComparisonEngine());
        objectHandler.merge(existing, incoming, ImportType.OVERWRITE_MINE, incomingToExisting);

        assertThat(existing.getPrivileges().size(), is(4));
        assertThat(existing.getPrivileges(), containsInAnyOrder(p1, p2, p3, p4));
    }

}