From 8c79c64d75522c337891a7d581a2d17e8af33b9e Mon Sep 17 00:00:00 2001 From: rfc2822 Date: Fri, 6 Dec 2013 14:17:39 +0100 Subject: [PATCH] Add more vCard fields * support for vCard organization (company / job title) * support for vCard IMPP addresses (closes #105) * refactoring --- res/xml/contacts.xml | 46 ++-- src/at/bitfire/davdroid/resource/Contact.java | 50 ++++- src/at/bitfire/davdroid/resource/Event.java | 6 + .../davdroid/resource/LocalAddressBook.java | 200 ++++++++++++++---- .../davdroid/resource/LocalCalendar.java | 34 +-- .../davdroid/resource/LocalCollection.java | 51 +++-- .../bitfire/davdroid/resource/Resource.java | 3 + .../davdroid/syncadapter/SyncManager.java | 2 +- 8 files changed, 283 insertions(+), 109 deletions(-) diff --git a/res/xml/contacts.xml b/res/xml/contacts.xml index 57aea051..cfd5e6c9 100644 --- a/res/xml/contacts.xml +++ b/res/xml/contacts.xml @@ -20,21 +20,35 @@ maxOccurs="1" /> - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + - - - + + + + + @@ -56,10 +70,8 @@ - + + diff --git a/src/at/bitfire/davdroid/resource/Contact.java b/src/at/bitfire/davdroid/resource/Contact.java index 439413ce..009d6111 100644 --- a/src/at/bitfire/davdroid/resource/Contact.java +++ b/src/at/bitfire/davdroid/resource/Contact.java @@ -35,11 +35,14 @@ import ezvcard.property.Anniversary; import ezvcard.property.Birthday; import ezvcard.property.Email; import ezvcard.property.FormattedName; +import ezvcard.property.Impp; import ezvcard.property.Nickname; import ezvcard.property.Note; +import ezvcard.property.Organization; import ezvcard.property.Photo; import ezvcard.property.RawProperty; import ezvcard.property.Revision; +import ezvcard.property.Role; import ezvcard.property.StructuredName; import ezvcard.property.Telephone; import ezvcard.property.Uid; @@ -71,12 +74,20 @@ public class Contact extends Resource { @Getter @Setter private String prefix, givenName, middleName, familyName, suffix; @Getter @Setter private String phoneticGivenName, phoneticMiddleName, phoneticFamilyName; @Getter @Setter private String note, URL; + @Getter @Setter private String organization, role; @Getter @Setter private byte[] photo; @Getter @Setter private Anniversary anniversary; @Getter @Setter private Birthday birthDay; + @Getter private List emails = new LinkedList(); + @Getter private List phoneNumbers = new LinkedList(); + @Getter private List
addresses = new LinkedList
(); + @Getter private List impps = new LinkedList(); + + + /* instance methods */ public Contact(String name, String ETag) { super(name, ETag); @@ -87,12 +98,11 @@ public class Contact extends Resource { this.localID = localID; } - - /* multiple-record fields */ - - @Getter private List emails = new LinkedList(); - @Getter private List phoneNumbers = new LinkedList(); - @Getter private List
addresses = new LinkedList
(); + @Override + public void initialize() { + uid = UUID.randomUUID().toString(); + name = uid + ".vcf"; + } /* VCard methods */ @@ -155,6 +165,17 @@ public class Contact extends Resource { } } + if (vcard.getOrganization() != null) { + List organizations = vcard.getOrganization().getValues(); + if (!organizations.isEmpty()) + organization = organizations.get(0); + } + List roles = vcard.getRoles(); + if (!roles.isEmpty()) + role = roles.get(0).getValue(); + + impps = vcard.getImpps(); + Nickname nicknames = vcard.getNickname(); if (nicknames != null && nicknames.getValues() != null) nickName = StringUtils.join(nicknames.getValues(), ", "); @@ -221,17 +242,28 @@ public class Contact extends Resource { if (photo != null) vcard.addPhoto(new Photo(photo, ImageType.JPEG)); + + if (organization != null) { + Organization org = new Organization(); + org.addValue(organization); + vcard.addOrganization(org); + } + if (role != null) + vcard.addRole(role); + + for (Impp impp : impps) + vcard.addImpp(impp); - if (nickName != null) + if (nickName != null && !nickName.isEmpty()) vcard.setNickname(nickName); - if (note != null) + if (note != null && !note.isEmpty()) vcard.addNote(note); for (Address address : addresses) vcard.addAddress(address); - if (URL != null) + if (URL != null && !URL.isEmpty()) vcard.addUrl(URL); if (anniversary != null) diff --git a/src/at/bitfire/davdroid/resource/Event.java b/src/at/bitfire/davdroid/resource/Event.java index 3a70730b..10f2dbfb 100644 --- a/src/at/bitfire/davdroid/resource/Event.java +++ b/src/at/bitfire/davdroid/resource/Event.java @@ -101,6 +101,12 @@ public class Event extends Resource { this(name, ETag); this.localID = localID; } + + @Override + public void initialize() { + uid = DavSyncAdapter.generateUID(); + name = uid.replace("@", "_") + ".ics"; + } @Override diff --git a/src/at/bitfire/davdroid/resource/LocalAddressBook.java b/src/at/bitfire/davdroid/resource/LocalAddressBook.java index eb4d6742..fdb80e1a 100644 --- a/src/at/bitfire/davdroid/resource/LocalAddressBook.java +++ b/src/at/bitfire/davdroid/resource/LocalAddressBook.java @@ -7,11 +7,11 @@ ******************************************************************************/ package at.bitfire.davdroid.resource; +import java.text.SimpleDateFormat; import java.util.LinkedList; import java.util.List; import java.util.Locale; import java.util.Set; -import java.util.UUID; import org.apache.commons.lang.StringUtils; import org.apache.commons.lang.WordUtils; @@ -28,8 +28,10 @@ import android.net.Uri; import android.os.RemoteException; import android.provider.ContactsContract.CommonDataKinds; import android.provider.ContactsContract.CommonDataKinds.Email; +import android.provider.ContactsContract.CommonDataKinds.Im; import android.provider.ContactsContract.CommonDataKinds.Nickname; import android.provider.ContactsContract.CommonDataKinds.Note; +import android.provider.ContactsContract.CommonDataKinds.Organization; import android.provider.ContactsContract.CommonDataKinds.Phone; import android.provider.ContactsContract.CommonDataKinds.Photo; import android.provider.ContactsContract.CommonDataKinds.StructuredName; @@ -40,16 +42,18 @@ import android.provider.ContactsContract.RawContacts; import at.bitfire.davdroid.Constants; import ezvcard.parameter.AddressType; import ezvcard.parameter.EmailType; +import ezvcard.parameter.ImppType; import ezvcard.parameter.TelephoneType; import ezvcard.property.Address; import ezvcard.property.Anniversary; import ezvcard.property.Birthday; import ezvcard.property.DateOrTimeProperty; +import ezvcard.property.Impp; import ezvcard.property.Telephone; public class LocalAddressBook extends LocalCollection { - private final static String TAG = "davdroid.LocalAddressBook"; + //private final static String TAG = "davdroid.LocalAddressBook"; protected AccountManager accountManager; @@ -101,25 +105,6 @@ public class LocalAddressBook extends LocalCollection { /* content provider (= database) querying */ - - @Override - public Contact findById(long localID, String remoteName, String eTag, boolean populate) throws RemoteException { - Contact c = new Contact(localID, remoteName, eTag); - if (populate) - populate(c); - return c; - } - - @Override - public Contact findByRemoteName(String remoteName) throws RemoteException { - Cursor cursor = providerClient.query(entriesURI(), - new String[] { RawContacts._ID, entryColumnRemoteName(), entryColumnETag() }, - entryColumnRemoteName() + "=?", new String[] { remoteName }, null); - if (cursor != null && cursor.moveToNext()) - return new Contact(cursor.getLong(0), cursor.getString(1), cursor.getString(2)); - else - return null; - } @Override public void populate(Resource res) throws RemoteException { @@ -254,11 +239,77 @@ public class LocalAddressBook extends LocalCollection { // photo cursor = providerClient.query(dataURI(), new String[] { Photo.PHOTO }, - Photo.RAW_CONTACT_ID + "=? AND " + Data.MIMETYPE + "=?", - new String[] { String.valueOf(c.getLocalID()), Photo.CONTENT_ITEM_TYPE }, null); + Photo.RAW_CONTACT_ID + "=? AND " + Data.MIMETYPE + "=?", + new String[] { String.valueOf(c.getLocalID()), Photo.CONTENT_ITEM_TYPE }, null); if (cursor != null && cursor.moveToNext()) c.setPhoto(cursor.getBlob(0)); + // organization + cursor = providerClient.query(dataURI(), new String[] { Organization.COMPANY, Organization.TITLE }, + Photo.RAW_CONTACT_ID + "=? AND " + Data.MIMETYPE + "=?", + new String[] { String.valueOf(c.getLocalID()), Organization.CONTENT_ITEM_TYPE }, null); + if (cursor != null && cursor.moveToNext()) { + c.setOrganization(cursor.getString(0)); + c.setRole(cursor.getString(1)); + } + + // IMPPs + cursor = providerClient.query(dataURI(), new String[] { Im.DATA, Im.TYPE, Im.LABEL, Im.PROTOCOL, Im.CUSTOM_PROTOCOL }, + Photo.RAW_CONTACT_ID + "=? AND " + Data.MIMETYPE + "=?", + new String[] { String.valueOf(c.getLocalID()), Im.CONTENT_ITEM_TYPE }, null); + while (cursor != null && cursor.moveToNext()) { + String handle = cursor.getString(0); + + Impp impp = null; + switch (cursor.getInt(3)) { + case Im.PROTOCOL_AIM: + impp = Impp.aim(handle); + break; + case Im.PROTOCOL_MSN: + impp = Impp.msn(handle); + break; + case Im.PROTOCOL_YAHOO: + impp = Impp.yahoo(handle); + break; + case Im.PROTOCOL_SKYPE: + impp = Impp.skype(handle); + break; + case Im.PROTOCOL_QQ: + impp = new Impp("qq", handle); + break; + case Im.PROTOCOL_GOOGLE_TALK: + impp = new Impp("google-talk", handle); + break; + case Im.PROTOCOL_ICQ: + impp = Impp.icq(handle); + break; + case Im.PROTOCOL_JABBER: + impp = Impp.xmpp(handle); + break; + case Im.PROTOCOL_NETMEETING: + impp = new Impp("netmeeting", handle); + break; + case Im.PROTOCOL_CUSTOM: + impp = new Impp(cursor.getString(4), handle); + break; + } + + if (impp != null) { + switch (cursor.getInt(1)) { + case Im.TYPE_HOME: + impp.addType(ImppType.HOME); + break; + case Im.TYPE_WORK: + impp.addType(ImppType.WORK); + break; + case Im.TYPE_CUSTOM: + impp.addType(ImppType.get(labelToXName(cursor.getString(2)))); + break; + } + c.getImpps().add(impp); + } + } + // nick name (max. 1) cursor = providerClient.query(dataURI(), new String[] { Nickname.NAME }, Nickname.RAW_CONTACT_ID + "=? AND " + Data.MIMETYPE + "=?", @@ -348,20 +399,17 @@ public class LocalAddressBook extends LocalCollection { .withYieldAllowed(true) .build()); } - - /* private helper methods */ - @Override - protected String fileExtension() { - return ".vcf"; - } + /* create/update/delete */ - @Override - protected String randomUID() { - return UUID.randomUUID().toString(); + public Contact newResource(long localID, String resourceName, String eTag) { + return new Contact(localID, resourceName, eTag); } + + /* private helper methods */ + protected Uri dataURI() { return syncAdapterURI(Data.CONTENT_URI); } @@ -416,8 +464,11 @@ public class LocalAddressBook extends LocalCollection { if (contact.getPhoto() != null) pendingOperations.add(buildPhoto(newDataInsertBuilder(localID, backrefIdx), contact.getPhoto()).build()); - // TODO organization - // TODO im + if (contact.getOrganization() != null || contact.getRole() != null) + pendingOperations.add(buildOrganization(newDataInsertBuilder(localID, backrefIdx), contact.getOrganization(), contact.getRole()).build()); + + for (Impp impp : contact.getImpps()) + pendingOperations.add(buildIMPP(newDataInsertBuilder(localID, backrefIdx), impp).build()); if (contact.getNickName() != null) pendingOperations.add(buildNickName(newDataInsertBuilder(localID, backrefIdx), contact.getNickName()).build()); @@ -530,7 +581,7 @@ public class LocalAddressBook extends LocalCollection { } protected Builder buildEmail(Builder builder, ezvcard.property.Email email) { - int typeCode = Email.TYPE_OTHER; + int typeCode = 0; String typeLabel = null; for (EmailType type : email.getTypes()) @@ -540,10 +591,14 @@ public class LocalAddressBook extends LocalCollection { typeCode = Email.TYPE_WORK; else if (type == Contact.EMAIL_TYPE_MOBILE) typeCode = Email.TYPE_MOBILE; + if (typeCode == 0) { + if (email.getTypes().isEmpty()) + typeCode = Email.TYPE_OTHER; else { typeCode = Email.TYPE_CUSTOM; - typeLabel = xNameToLabel(type.getValue()); + typeLabel = xNameToLabel(email.getTypes().iterator().next().getValue()); } + } builder = builder .withValue(Data.MIMETYPE, Email.CONTENT_ITEM_TYPE) @@ -560,6 +615,69 @@ public class LocalAddressBook extends LocalCollection { .withValue(Photo.PHOTO, photo); } + protected Builder buildOrganization(Builder builder, String organization, String role) { + return builder + .withValue(Data.MIMETYPE, Organization.CONTENT_ITEM_TYPE) + .withValue(Organization.COMPANY, organization) + .withValue(Organization.TITLE, role); + } + + protected Builder buildIMPP(Builder builder, Impp impp) { + int typeCode = 0; + String typeLabel = null; + for (ImppType type : impp.getTypes()) + if (type == ImppType.HOME) + typeCode = Im.TYPE_HOME; + else if (type == ImppType.WORK || type == ImppType.BUSINESS) + typeCode = Im.TYPE_WORK; + if (typeCode == 0) + if (impp.getTypes().isEmpty()) + typeCode = Im.TYPE_OTHER; + else { + typeCode = Im.TYPE_CUSTOM; + typeLabel = xNameToLabel(impp.getTypes().iterator().next().getValue()); + } + + int protocolCode; + String protocolLabel = null; + if (impp.isAim()) + protocolCode = Im.PROTOCOL_AIM; + else if (impp.isMsn()) + protocolCode = Im.PROTOCOL_MSN; + else if (impp.isYahoo()) + protocolCode = Im.PROTOCOL_YAHOO; + else if (impp.isSkype()) + protocolCode = Im.PROTOCOL_SKYPE; + else if (impp.getProtocol().equalsIgnoreCase("qq")) + protocolCode = Im.PROTOCOL_QQ; + else if (impp.getProtocol().equalsIgnoreCase("google-talk")) + protocolCode = Im.PROTOCOL_GOOGLE_TALK; + else if (impp.isIcq()) + protocolCode = Im.PROTOCOL_ICQ; + else if (impp.isXmpp()) + protocolCode = Im.PROTOCOL_JABBER; + else if (impp.getProtocol().equalsIgnoreCase("netmeeting")) + protocolCode = Im.PROTOCOL_NETMEETING; + else { + String protocol = impp.getProtocol(); + if (protocol == null) + protocol = "unknown"; + protocolCode = Im.PROTOCOL_CUSTOM; + protocolLabel = protocol; + } + + builder = builder + .withValue(Data.MIMETYPE, Im.CONTENT_ITEM_TYPE) + .withValue(Im.DATA, impp.getHandle()) + .withValue(Im.TYPE, typeCode) + .withValue(Im.PROTOCOL, protocolCode); + if (typeLabel != null) + builder = builder.withValue(Im.LABEL, typeLabel); + if (protocolLabel != null) + builder = builder.withValue(Im.CUSTOM_PROTOCOL, protocolLabel); + return builder; + } + protected Builder buildNickName(Builder builder, String nickName) { return builder .withValue(Data.MIMETYPE, Nickname.CONTENT_ITEM_TYPE) @@ -594,16 +712,19 @@ public class LocalAddressBook extends LocalCollection { formattedAddress = StringUtils.join(lines, "\n"); } - int typeCode = StructuredPostal.TYPE_OTHER; + int typeCode = 0; String typeLabel = null; for (AddressType type : address.getTypes()) if (type == AddressType.HOME) typeCode = StructuredPostal.TYPE_HOME; else if (type == AddressType.WORK) typeCode = StructuredPostal.TYPE_WORK; + if (typeCode == 0) + if (address.getTypes().isEmpty()) + typeCode = StructuredPostal.TYPE_OTHER; else { typeCode = StructuredPostal.TYPE_CUSTOM; - typeLabel = xNameToLabel(type.getValue()); + typeLabel = xNameToLabel(address.getTypes().iterator().next().getValue()); } builder = builder @@ -629,9 +750,10 @@ public class LocalAddressBook extends LocalCollection { } protected Builder buildEvent(Builder builder, DateOrTimeProperty date, int type) { + SimpleDateFormat formatter = new SimpleDateFormat("yyyy-MM-dd", Locale.US); return builder .withValue(Data.MIMETYPE, CommonDataKinds.Event.CONTENT_ITEM_TYPE) .withValue(CommonDataKinds.Event.TYPE, type) - .withValue(CommonDataKinds.Event.START_DATE, date.getText()); + .withValue(CommonDataKinds.Event.START_DATE, formatter.format(date.getDate())); } } diff --git a/src/at/bitfire/davdroid/resource/LocalCalendar.java b/src/at/bitfire/davdroid/resource/LocalCalendar.java index f8df4c80..aff732c6 100644 --- a/src/at/bitfire/davdroid/resource/LocalCalendar.java +++ b/src/at/bitfire/davdroid/resource/LocalCalendar.java @@ -60,7 +60,6 @@ import android.provider.CalendarContract.Events; import android.provider.CalendarContract.Reminders; import android.provider.ContactsContract; import android.util.Log; -import at.bitfire.davdroid.syncadapter.DavSyncAdapter; import at.bitfire.davdroid.syncadapter.ServerInfo; public class LocalCalendar extends LocalCollection { @@ -167,26 +166,6 @@ public class LocalCalendar extends LocalCollection { /* content provider (= database) querying */ - - @Override - public Event findById(long localID, String remoteName, String eTag, boolean populate) throws RemoteException { - Event e = new Event(localID, remoteName, eTag); - if (populate) - populate(e); - return e; - } - - @Override - public Event findByRemoteName(String remoteName) throws RemoteException { - Cursor cursor = providerClient.query(entriesURI(), - new String[] { entryColumnID(), entryColumnRemoteName(), entryColumnETag() }, - Events.CALENDAR_ID + "=? AND " + entryColumnRemoteName() + "=?", - new String[] { String.valueOf(id), remoteName }, null); - if (cursor != null && cursor.moveToNext()) - return new Event(cursor.getLong(0), cursor.getString(1), cursor.getString(2)); - else - return null; - } @Override public void populate(Resource resource) throws RemoteException { @@ -398,17 +377,14 @@ public class LocalCalendar extends LocalCollection { } - /* private helper methods */ + /* create/update/delete */ - @Override - protected String fileExtension() { - return ".ics"; + public Event newResource(long localID, String resourceName, String eTag) { + return new Event(localID, resourceName, eTag); } - @Override - protected String randomUID() { - return DavSyncAdapter.generateUID(); - } + + /* private helper methods */ protected static Uri calendarsURI(Account account) { return Calendars.CONTENT_URI.buildUpon().appendQueryParameter(Calendars.ACCOUNT_NAME, account.name) diff --git a/src/at/bitfire/davdroid/resource/LocalCollection.java b/src/at/bitfire/davdroid/resource/LocalCollection.java index d93c171b..82e572da 100644 --- a/src/at/bitfire/davdroid/resource/LocalCollection.java +++ b/src/at/bitfire/davdroid/resource/LocalCollection.java @@ -80,7 +80,7 @@ public abstract class LocalCollection { where, null, null); LinkedList dirty = new LinkedList(); while (cursor != null && cursor.moveToNext()) - dirty.add(findById(cursor.getLong(0), cursor.getString(1), cursor.getString(2), true)); + dirty.add(findById(cursor.getLong(0), true)); return dirty.toArray(new Resource[0]); } @@ -93,7 +93,7 @@ public abstract class LocalCollection { where, null, null); LinkedList deleted = new LinkedList(); while (cursor != null && cursor.moveToNext()) - deleted.add(findById(cursor.getLong(0), cursor.getString(1), cursor.getString(2), false)); + deleted.add(findById(cursor.getLong(0), false)); return deleted.toArray(new Resource[0]); } @@ -106,15 +106,17 @@ public abstract class LocalCollection { where, null, null); LinkedList fresh = new LinkedList(); while (cursor != null && cursor.moveToNext()) { - String uid = randomUID(), - resourceName = uid.replace("@", "_") + fileExtension(); - T resource = findById(cursor.getLong(0), resourceName, null, true); - resource.setUid(uid); + T resource = findById(cursor.getLong(0), true); + /*String uid = randomUID(), + resourceName = uid.replace("@", "_") + fileExtension(); + resource.setUid(uid);*/ + resource.initialize(); // new record: set generated UID + remote file name in database pendingOperations.add(ContentProviderOperation .newUpdate(ContentUris.withAppendedId(entriesURI(), resource.getLocalID())) - .withValue(entryColumnRemoteName(), resourceName) + .withValue(entryColumnUID(), resource.getUid()) + .withValue(entryColumnRemoteName(), resource.getName()) .build()); fresh.add(resource); @@ -122,14 +124,39 @@ public abstract class LocalCollection { return fresh.toArray(new Resource[0]); } - abstract public T findById(long localID, String resourceName, String eTag, boolean populate) throws RemoteException; - abstract public T findByRemoteName(String name) throws RemoteException; + public T findById(long localID, boolean populate) throws RemoteException { + Cursor cursor = providerClient.query(ContentUris.withAppendedId(entriesURI(), localID), + new String[] { entryColumnRemoteName(), entryColumnETag() }, null, null, null); + if (cursor != null && cursor.moveToNext()) { + T resource = newResource(localID, cursor.getString(0), cursor.getString(1)); + if (populate) + populate(resource); + return resource; + } else + return null; + } + + public T findByRemoteName(String remoteName, boolean populate) throws RemoteException { + Cursor cursor = providerClient.query(entriesURI(), + new String[] { entryColumnID(), entryColumnRemoteName(), entryColumnETag() }, + entryColumnRemoteName() + "=?", new String[] { remoteName }, null); + if (cursor != null && cursor.moveToNext()) { + T resource = newResource(cursor.getLong(0), cursor.getString(1), cursor.getString(2)); + if (populate) + populate(resource); + return resource; + } else + return null; + } + public abstract void populate(Resource record) throws RemoteException; // create/update/delete + abstract public T newResource(long localID, String resourceName, String eTag); + public void add(Resource resource) { int idx = pendingOperations.size(); pendingOperations.add( @@ -141,7 +168,7 @@ public abstract class LocalCollection { } public void updateByRemoteName(Resource remoteResource) throws RemoteException, ValidationException { - T localResource = findByRemoteName(remoteResource.getName()); + T localResource = findByRemoteName(remoteResource.getName(), false); pendingOperations.add( buildEntry(ContentProviderOperation.newUpdate(ContentUris.withAppendedId(entriesURI(), localResource.getLocalID())), remoteResource) @@ -179,10 +206,6 @@ public abstract class LocalCollection { // helpers - protected abstract String fileExtension(); - - protected abstract String randomUID(); - protected Uri syncAdapterURI(Uri baseURI) { return baseURI.buildUpon() .appendQueryParameter(entryColumnAccountType(), account.type) diff --git a/src/at/bitfire/davdroid/resource/Resource.java b/src/at/bitfire/davdroid/resource/Resource.java index e4b839ff..ff6b6a8b 100644 --- a/src/at/bitfire/davdroid/resource/Resource.java +++ b/src/at/bitfire/davdroid/resource/Resource.java @@ -36,6 +36,9 @@ public abstract class Resource { this.localID = localID; } + // sets resource name and UID + public abstract void initialize(); + public abstract void parseEntity(InputStream entity) throws IOException, ParserException, VCardException; public abstract String toEntity() throws IOException, ValidationException; } diff --git a/src/at/bitfire/davdroid/syncadapter/SyncManager.java b/src/at/bitfire/davdroid/syncadapter/SyncManager.java index a48c7b49..df4eb006 100644 --- a/src/at/bitfire/davdroid/syncadapter/SyncManager.java +++ b/src/at/bitfire/davdroid/syncadapter/SyncManager.java @@ -128,7 +128,7 @@ public class SyncManager { return; for (Resource remoteResource : remoteResources) { - Resource localResource = local.findByRemoteName(remoteResource.getName()); + Resource localResource = local.findByRemoteName(remoteResource.getName(), true); if (localResource == null) resourcesToAdd.add(remoteResource); else if (localResource.getETag() == null || !localResource.getETag().equals(remoteResource.getETag()))