From 0a92d7e1f4215528ae33103ca3081b72c3a69407 Mon Sep 17 00:00:00 2001 From: desperateCoder Date: Sun, 22 Nov 2020 16:38:56 +0100 Subject: #359 etags: first steps - board list and board --- .../it/niedermann/nextcloud/deck/api/DeckAPI.java | 5 +- .../nextcloud/deck/api/JsonToEntityParser.java | 62 ++++++++++++---------- .../niedermann/nextcloud/deck/model/Account.java | 9 ++++ .../model/interfaces/AbstractRemoteEntity.java | 11 ++++ .../deck/model/interfaces/IRemoteEntity.java | 8 +++ .../persistence/sync/adapters/ServerAdapter.java | 4 +- .../persistence/sync/adapters/db/DeckDatabase.java | 22 +++++++- .../deck/persistence/sync/helpers/SyncHelper.java | 21 ++++++-- .../sync/helpers/providers/BoardDataProvider.java | 24 ++++++++- 9 files changed, 128 insertions(+), 38 deletions(-) diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/api/DeckAPI.java b/app/src/main/java/it/niedermann/nextcloud/deck/api/DeckAPI.java index a303a8a33..1f0148b91 100644 --- a/app/src/main/java/it/niedermann/nextcloud/deck/api/DeckAPI.java +++ b/app/src/main/java/it/niedermann/nextcloud/deck/api/DeckAPI.java @@ -1,6 +1,8 @@ package it.niedermann.nextcloud.deck.api; +import com.nextcloud.android.sso.api.ParsedResponse; + import java.util.List; import io.reactivex.Observable; @@ -33,6 +35,7 @@ import retrofit2.http.Query; public interface DeckAPI { String MODIFIED_SINCE_HEADER = "If-Modified-Since"; + String IF_NONE_MATCH = "If-None-Match"; // ### BOARDS @POST("boards") @@ -51,7 +54,7 @@ public interface DeckAPI { Observable restoreBoard(@Path("id") long id); @GET("boards") - Observable> getBoards(@Query ("details") boolean verbose, @Header(MODIFIED_SINCE_HEADER) String lastSync ); + Observable>> getBoards(@Query ("details") boolean verbose, @Header(MODIFIED_SINCE_HEADER) String lastSync, @Header(IF_NONE_MATCH) String eTag); // ### Stacks diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/api/JsonToEntityParser.java b/app/src/main/java/it/niedermann/nextcloud/deck/api/JsonToEntityParser.java index 8601cab41..22d84850e 100644 --- a/app/src/main/java/it/niedermann/nextcloud/deck/api/JsonToEntityParser.java +++ b/app/src/main/java/it/niedermann/nextcloud/deck/api/JsonToEntityParser.java @@ -284,6 +284,7 @@ public class JsonToEntityParser { makeTraceableIfFails(() -> { board.setTitle(getNullAsEmptyString(e.get("title"))); board.setColor(getNullAsEmptyString(e.get("color"))); + board.setEtag(getNullAsEmptyString(e.get("ETag"))); board.setArchived(e.get("archived").getAsBoolean()); board.setLastModified(getTimestampFromLong(e.get("lastModified"))); @@ -396,6 +397,7 @@ public class JsonToEntityParser { card.setDescription(getNullAsEmptyString(e.get("description"))); card.setStackId(e.get("stackId").getAsLong()); card.setType(getNullAsEmptyString(e.get("type"))); + card.setEtag(getNullAsEmptyString(e.get("ETag"))); card.setLastModified(getTimestampFromLong(e.get("lastModified"))); card.setCreatedAt(getTimestampFromLong(e.get("createdAt"))); card.setDeletedAt(getTimestampFromLong(e.get("deletedAt"))); @@ -456,6 +458,7 @@ public class JsonToEntityParser { a.setId(e.get("id").getAsLong()); a.setCardId(e.get("cardId").getAsLong()); a.setType(e.get("type").getAsString()); + a.setEtag(getNullAsEmptyString(e.get("ETag"))); a.setData(e.get("data").getAsString()); a.setLastModified(getTimestampFromLong(e.get("lastModified"))); a.setCreatedAt(getTimestampFromLong(e.get("createdAt"))); @@ -571,33 +574,6 @@ public class JsonToEntityParser { return Color.GRAY; } - protected static List parseActivity(JsonObject e) { - DeckLog.verbose(e.toString()); - List activityList = new ArrayList<>(); - - makeTraceableIfFails(() -> { - if (e.has("ocs")) { - JsonObject ocs = e.getAsJsonObject("ocs"); - if (ocs.has("data")) { - JsonArray data = ocs.getAsJsonArray("data"); - for (JsonElement activityJson : data) { - Activity activity = new Activity(); - JsonObject activityObject = activityJson.getAsJsonObject(); - - activity.setId(activityObject.get("activity_id").getAsLong()); - activity.setType(ActivityType.findByPath(getNullAsEmptyString(activityObject.get("icon"))).getId()); - activity.setSubject(getNullAsEmptyString(activityObject.get("subject"))); - activity.setCardId(activityObject.get("object_id").getAsLong()); - activity.setLastModified(getTimestampFromString(activityObject.get("datetime"))); - - activityList.add(activity); - } - } - } - }, e); - return activityList; - } - protected static FullStack parseStack(JsonObject e) { DeckLog.verbose(e.toString()); FullStack fullStack = new FullStack(); @@ -607,6 +583,7 @@ public class JsonToEntityParser { stack.setTitle(getNullAsEmptyString(e.get("title"))); stack.setBoardId(e.get("boardId").getAsLong()); stack.setId(e.get("id").getAsLong()); + stack.setEtag(getNullAsEmptyString(e.get("ETag"))); stack.setLastModified(getTimestampFromLong(e.get("lastModified"))); stack.setDeletedAt(getTimestampFromLong(e.get("deletedAt"))); if (e.has("order") && !e.get("order").isJsonNull()) { @@ -627,6 +604,34 @@ public class JsonToEntityParser { return fullStack; } + protected static List parseActivity(JsonObject e) { + DeckLog.verbose(e.toString()); + List activityList = new ArrayList<>(); + + makeTraceableIfFails(() -> { + if (e.has("ocs")) { + JsonObject ocs = e.getAsJsonObject("ocs"); + if (ocs.has("data")) { + JsonArray data = ocs.getAsJsonArray("data"); + for (JsonElement activityJson : data) { + Activity activity = new Activity(); + JsonObject activityObject = activityJson.getAsJsonObject(); + + activity.setId(activityObject.get("activity_id").getAsLong()); + activity.setType(ActivityType.findByPath(getNullAsEmptyString(activityObject.get("icon"))).getId()); + activity.setSubject(getNullAsEmptyString(activityObject.get("subject"))); + activity.setCardId(activityObject.get("object_id").getAsLong()); + activity.setEtag(getNullAsEmptyString(e.get("ETag"))); + activity.setLastModified(getTimestampFromString(activityObject.get("datetime"))); + + activityList.add(activity); + } + } + } + }, e); + return activityList; + } + protected static Label parseLabel(JsonObject e) { DeckLog.verbose(e.toString()); Label label = new Label(); @@ -635,13 +640,14 @@ public class JsonToEntityParser { //todo: last modified! // label.setLastModified(get); label.setTitle(getNullAsEmptyString(e.get("title"))); + label.setEtag(getNullAsEmptyString(e.get("ETag"))); label.setColor(getColorAsInt(e, "color")); }, e); return label; } private static String getNullAsEmptyString(JsonElement jsonElement) { - return jsonElement.isJsonNull() ? "" : jsonElement.getAsString(); + return jsonElement == null || jsonElement.isJsonNull() ? "" : jsonElement.getAsString(); } private static Instant getTimestampFromString(JsonElement jsonElement) { diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/model/Account.java b/app/src/main/java/it/niedermann/nextcloud/deck/model/Account.java index 1c189d162..8abcbced7 100644 --- a/app/src/main/java/it/niedermann/nextcloud/deck/model/Account.java +++ b/app/src/main/java/it/niedermann/nextcloud/deck/model/Account.java @@ -54,6 +54,7 @@ public class Account implements Serializable { private boolean maintenanceEnabled = false; private String etag; + private String boardsEtag; @Ignore public Account(Long id, @NonNull String name, @NonNull String userName, @NonNull String url) { @@ -190,6 +191,14 @@ public class Account implements Serializable { this.etag = etag; } + public String getBoardsEtag() { + return boardsEtag; + } + + public void setBoardsEtag(String boardsEtag) { + this.boardsEtag = boardsEtag; + } + /** * A cache buster parameter is added for duplicate account names on different hosts which shall be fetched from the same {@link SingleSignOnAccount} (e. g. {@link AccountSwitcherDialog}) * diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/model/interfaces/AbstractRemoteEntity.java b/app/src/main/java/it/niedermann/nextcloud/deck/model/interfaces/AbstractRemoteEntity.java index 54cd0fc31..2f7771383 100644 --- a/app/src/main/java/it/niedermann/nextcloud/deck/model/interfaces/AbstractRemoteEntity.java +++ b/app/src/main/java/it/niedermann/nextcloud/deck/model/interfaces/AbstractRemoteEntity.java @@ -39,6 +39,8 @@ public abstract class AbstractRemoteEntity implements IRemoteEntity { protected Instant lastModified; protected Instant lastModifiedLocal; + protected String etag; + public AbstractRemoteEntity() { } @@ -137,6 +139,15 @@ public abstract class AbstractRemoteEntity implements IRemoteEntity { this.status = status.getId(); } + @Override + public String getEtag() { + return etag; + } + + @Override + public void setEtag(String etag) { + this.etag = etag; + } @Override public boolean equals(Object o) { diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/model/interfaces/IRemoteEntity.java b/app/src/main/java/it/niedermann/nextcloud/deck/model/interfaces/IRemoteEntity.java index f50bf0b90..f197c0d30 100644 --- a/app/src/main/java/it/niedermann/nextcloud/deck/model/interfaces/IRemoteEntity.java +++ b/app/src/main/java/it/niedermann/nextcloud/deck/model/interfaces/IRemoteEntity.java @@ -68,6 +68,14 @@ public interface IRemoteEntity { getEntity().setStatusEnum(status); } + default String getEtag() { + return getEntity().getEtag(); + } + + default void setEtag(String etag) { + getEntity().setEtag(etag); + } + default List copyList(List listToCopy) { if (listToCopy == null) { return null; diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/persistence/sync/adapters/ServerAdapter.java b/app/src/main/java/it/niedermann/nextcloud/deck/persistence/sync/adapters/ServerAdapter.java index a0b9fd8aa..bff8ddfdb 100644 --- a/app/src/main/java/it/niedermann/nextcloud/deck/persistence/sync/adapters/ServerAdapter.java +++ b/app/src/main/java/it/niedermann/nextcloud/deck/persistence/sync/adapters/ServerAdapter.java @@ -125,9 +125,9 @@ public class ServerAdapter { // return lastSyncHeader; } - public void getBoards(IResponseCallback> responseCallback) { + public void getBoards(IResponseCallback>> responseCallback) { RequestHelper.request(provider, () -> - provider.getDeckAPI().getBoards(true, getLastSyncDateFormatted(responseCallback.getAccount().getId())), + provider.getDeckAPI().getBoards(true, getLastSyncDateFormatted(responseCallback.getAccount().getId()), responseCallback.getAccount().getBoardsEtag()), responseCallback); } diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/persistence/sync/adapters/db/DeckDatabase.java b/app/src/main/java/it/niedermann/nextcloud/deck/persistence/sync/adapters/db/DeckDatabase.java index 0ac0f786b..a491946ed 100644 --- a/app/src/main/java/it/niedermann/nextcloud/deck/persistence/sync/adapters/db/DeckDatabase.java +++ b/app/src/main/java/it/niedermann/nextcloud/deck/persistence/sync/adapters/db/DeckDatabase.java @@ -97,7 +97,7 @@ import it.niedermann.nextcloud.deck.persistence.sync.adapters.db.dao.widgets.Sta UserInBoard.class, }, exportSchema = false, - version = 22 + version = 23 ) @TypeConverters({DateTypeConverter.class}) public abstract class DeckDatabase extends RoomDatabase { @@ -375,6 +375,25 @@ public abstract class DeckDatabase extends RoomDatabase { } }; + private static final Migration MIGRATION_22_23 = new Migration(22, 23) { + @Override + public void migrate(SupportSQLiteDatabase database) { + // https://github.com/stefan-niedermann/nextcloud-deck/issues/359 + database.execSQL("ALTER TABLE `Account` ADD `boardsEtag` TEXT"); + database.execSQL("ALTER TABLE `Board` ADD `etag` TEXT"); + database.execSQL("ALTER TABLE `Stack` ADD `etag` TEXT"); + database.execSQL("ALTER TABLE `Card` ADD `etag` TEXT"); + database.execSQL("ALTER TABLE `Label` ADD `etag` TEXT"); + database.execSQL("ALTER TABLE `AccessControl` ADD `etag` TEXT"); + database.execSQL("ALTER TABLE `Attachment` ADD `etag` TEXT"); + database.execSQL("ALTER TABLE `User` ADD `etag` TEXT"); + database.execSQL("ALTER TABLE `DeckComment` ADD `etag` TEXT"); + database.execSQL("ALTER TABLE `Activity` ADD `etag` TEXT"); + database.execSQL("ALTER TABLE `OcsProject` ADD `etag` TEXT"); + database.execSQL("ALTER TABLE `OcsProjectResource` ADD `etag` TEXT"); + } + }; + public static final RoomDatabase.Callback ON_CREATE_CALLBACK = new RoomDatabase.Callback() { @Override public void onCreate(@NonNull SupportSQLiteDatabase db) { @@ -434,6 +453,7 @@ public abstract class DeckDatabase extends RoomDatabase { lastSyncPref.apply(); } }) + .addMigrations(MIGRATION_22_23) .fallbackToDestructiveMigration() .addCallback(ON_CREATE_CALLBACK) .build(); diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/persistence/sync/helpers/SyncHelper.java b/app/src/main/java/it/niedermann/nextcloud/deck/persistence/sync/helpers/SyncHelper.java index 032c3594a..69e3833cf 100644 --- a/app/src/main/java/it/niedermann/nextcloud/deck/persistence/sync/helpers/SyncHelper.java +++ b/app/src/main/java/it/niedermann/nextcloud/deck/persistence/sync/helpers/SyncHelper.java @@ -1,5 +1,8 @@ package it.niedermann.nextcloud.deck.persistence.sync.helpers; +import com.nextcloud.android.sso.exceptions.NextcloudHttpRequestFailedException; + +import java.net.HttpURLConnection; import java.time.Instant; import java.util.List; import java.util.concurrent.CountDownLatch; @@ -53,9 +56,10 @@ public class SyncHelper { DeckLog.log("Conflicting changes on entity: " + existingEntity); // TODO: what to do? } else { -// if (existingEntity.getLastModified().getTime() == entityFromServer.getLastModified().getTime()) { -// continue; // TODO: is this is ok for sure? -> isn`t! NPE -// } + if (entityFromServer.getEtag() != null && entityFromServer.getEtag().equals(existingEntity.getEtag())) { + DeckLog.info("ETags do match! skipping this one."); + continue; + } provider.updateInDB(dataBaseAdapter, accountId, applyUpdatesFromRemote(provider, existingEntity, entityFromServer, accountId), false); } } @@ -73,8 +77,17 @@ public class SyncHelper { @Override public void onError(Throwable throwable) { + super.onError(throwable); + if (throwable.getClass() == NextcloudHttpRequestFailedException.class) { + NextcloudHttpRequestFailedException requestFailedException = (NextcloudHttpRequestFailedException) throwable; + if (HttpURLConnection.HTTP_NOT_MODIFIED == requestFailedException.getStatusCode()){ + DeckLog.info("ETags do match! skipping this one."); + // well, etags say we're fine here. no need to go deeper. + provider.childDone(provider, responseCallback, false); + return; + } + } provider.onError(throwable, responseCallback); - DeckLog.logError(throwable); responseCallback.onError(throwable); } }, lastSync); diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/persistence/sync/helpers/providers/BoardDataProvider.java b/app/src/main/java/it/niedermann/nextcloud/deck/persistence/sync/helpers/providers/BoardDataProvider.java index 5b458471f..f5e071adb 100644 --- a/app/src/main/java/it/niedermann/nextcloud/deck/persistence/sync/helpers/providers/BoardDataProvider.java +++ b/app/src/main/java/it/niedermann/nextcloud/deck/persistence/sync/helpers/providers/BoardDataProvider.java @@ -1,5 +1,9 @@ package it.niedermann.nextcloud.deck.persistence.sync.helpers.providers; +import android.annotation.SuppressLint; + +import com.nextcloud.android.sso.api.ParsedResponse; + import java.time.Instant; import java.util.ArrayList; import java.util.Collections; @@ -26,8 +30,24 @@ public class BoardDataProvider extends AbstractSyncDataProvider { } @Override - public void getAllFromServer(ServerAdapter serverAdapter, long accountId, IResponseCallback> responder, Instant lastSync) { - serverAdapter.getBoards(responder); + public void getAllFromServer(ServerAdapter serverAdapter, DataBaseAdapter dataBaseAdapter, long accountId, IResponseCallback> responder, Instant lastSync) { + serverAdapter.getBoards(new IResponseCallback>>(responder.getAccount()) { + @Override + public void onResponse(ParsedResponse> response) { + String etag = response.getHeaders().get("ETag"); + if (etag != null && !etag.equals(account.getBoardsEtag())) { + account.setBoardsEtag(etag); + dataBaseAdapter.updateAccount(account); + } + responder.onResponse(response.getResponse()); + } + + @SuppressLint("MissingSuperCall") + @Override + public void onError(Throwable throwable) { + responder.onError(throwable); + } + }); } @Override -- cgit v1.2.3