Enhance handling, parsing, sanitizing and serialization of supported ApiVersions

This commit is contained in:
Stefan Niedermann 2021-05-11 20:28:50 +02:00 committed by Niedermann IT-Dienstleistungen
parent bf668410a5
commit 59c99f7de4
10 changed files with 381 additions and 89 deletions

View file

@ -47,6 +47,7 @@ import it.niedermann.owncloud.notes.persistence.entity.Note;
import it.niedermann.owncloud.notes.shared.model.ApiVersion;
import it.niedermann.owncloud.notes.shared.model.DBStatus;
import it.niedermann.owncloud.notes.shared.model.ISyncCallback;
import it.niedermann.owncloud.notes.shared.util.ApiVersionUtil;
import it.niedermann.owncloud.notes.shared.util.NoteUtil;
import it.niedermann.owncloud.notes.shared.util.NotesColorUtil;
import it.niedermann.owncloud.notes.shared.util.ShareUtil;
@ -193,7 +194,8 @@ public abstract class BaseNoteFragment extends BrandedFragment implements Catego
if (note != null) {
prepareFavoriteOption(menu.findItem(R.id.menu_favorite));
menu.findItem(R.id.menu_title).setVisible(localAccount.getPreferredApiVersion() != null && localAccount.getPreferredApiVersion().compareTo(ApiVersion.API_VERSION_1_0) >= 0);
final ApiVersion preferredApiVersion = ApiVersionUtil.getPreferredApiVersion(localAccount.getApiVersion());
menu.findItem(R.id.menu_title).setVisible(preferredApiVersion != null && preferredApiVersion.compareTo(ApiVersion.API_VERSION_1_0) >= 0);
menu.findItem(R.id.menu_delete).setVisible(!isNew);
}
}

View file

@ -30,11 +30,9 @@ import com.nextcloud.android.sso.exceptions.NoCurrentAccountSelectedException;
import com.nextcloud.android.sso.helper.SingleAccountHelper;
import com.nextcloud.android.sso.model.SingleSignOnAccount;
import org.json.JSONArray;
import org.json.JSONException;
import java.util.ArrayList;
import java.util.Calendar;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
@ -61,6 +59,7 @@ import it.niedermann.owncloud.notes.shared.model.IResponseCallback;
import it.niedermann.owncloud.notes.shared.model.ISyncCallback;
import it.niedermann.owncloud.notes.shared.model.NavigationCategory;
import it.niedermann.owncloud.notes.shared.model.SyncResultStatus;
import it.niedermann.owncloud.notes.shared.util.ApiVersionUtil;
import it.niedermann.owncloud.notes.shared.util.NoteUtil;
import it.niedermann.owncloud.notes.shared.util.SSOUtil;
@ -462,8 +461,7 @@ public class NotesRepository {
final Note newNote;
// Re-read the up to date remoteId from the database because the UI might not have the state after synchronization yet
// https://github.com/stefan-niedermann/nextcloud-notes/issues/1198
@Nullable
final Long remoteId = db.getNoteDao().getRemoteId(oldNote.getId());
@Nullable final Long remoteId = db.getNoteDao().getRemoteId(oldNote.getId());
if (newContent == null) {
newNote = new Note(oldNote.getId(), remoteId, oldNote.getModified(), oldNote.getTitle(), oldNote.getContent(), oldNote.getCategory(), oldNote.getFavorite(), oldNote.getETag(), DBStatus.LOCAL_EDITED, localAccount.getId(), oldNote.getExcerpt(), oldNote.getScrollY());
} else {
@ -471,7 +469,8 @@ public class NotesRepository {
if (newTitle != null) {
title = newTitle;
} else {
if ((remoteId == null || localAccount.getPreferredApiVersion() == null || localAccount.getPreferredApiVersion().compareTo(ApiVersion.API_VERSION_1_0) < 0) &&
final ApiVersion preferredApiVersion = ApiVersionUtil.getPreferredApiVersion(localAccount.getApiVersion());
if ((remoteId == null || preferredApiVersion == null || preferredApiVersion.compareTo(ApiVersion.API_VERSION_1_0) < 0) &&
(defaultNonEmptyTitle.equals(oldNote.getTitle()))) {
title = NoteUtil.generateNonEmptyNoteTitle(newContent, context);
} else {
@ -573,40 +572,23 @@ public class NotesRepository {
}
/**
* @param apiVersion has to be a JSON array as a string <code>["0.2", "1.0", ...]</code>
* @return whether or not the given {@link ApiVersion} has been written to the database
* @throws IllegalArgumentException if the apiVersion does not match the expected format
* @param raw has to be a JSON array as a string <code>["0.2", "1.0", ...]</code>
*/
public boolean updateApiVersion(long accountId, @Nullable String apiVersion) throws IllegalArgumentException {
if (apiVersion != null) {
try {
JSONArray apiVersions = new JSONArray(apiVersion);
for (int i = 0; i < apiVersions.length(); i++) {
ApiVersion.of(apiVersions.getString(i));
}
if (apiVersions.length() > 0) {
final int updatedRows = db.getAccountDao().updateApiVersion(accountId, apiVersion);
if (updatedRows == 0) {
Log.d(TAG, "ApiVersion not updated, because it did not change");
} else if (updatedRows == 1) {
Log.i(TAG, "Updated apiVersion to \"" + apiVersion + "\" for accountId = " + accountId);
ApiProvider.invalidateAPICache();
} else {
Log.w(TAG, "Updated " + updatedRows + " but expected only 1 for accountId = " + accountId + " and apiVersion = \"" + apiVersion + "\"");
}
return true;
} else {
Log.i(TAG, "Given API version is a valid JSON array but does not contain any valid API versions. Do not update database.");
}
} catch (NumberFormatException e) {
throw new IllegalArgumentException("API version does contain a non-valid version: " + apiVersion);
} catch (JSONException e) {
throw new IllegalArgumentException("API version must contain be a JSON array: " + apiVersion);
public void updateApiVersion(long accountId, @Nullable String raw) {
final Collection<ApiVersion> apiVersions = ApiVersionUtil.parse(raw);
if (apiVersions.size() > 0) {
final int updatedRows = db.getAccountDao().updateApiVersion(accountId, ApiVersionUtil.serialize(apiVersions));
if (updatedRows == 0) {
Log.d(TAG, "ApiVersion not updated, because it did not change");
} else if (updatedRows == 1) {
Log.i(TAG, "Updated apiVersion to \"" + raw + "\" for accountId = " + accountId);
ApiProvider.invalidateAPICache();
} else {
Log.w(TAG, "Updated " + updatedRows + " but expected only 1 for accountId = " + accountId + " and apiVersion = \"" + raw + "\"");
}
} else {
Log.v(TAG, "Given API version is null. Do not update database");
Log.v(TAG, "Could not extract any version from the given String: " + raw);
}
return false;
}
/**
@ -785,7 +767,7 @@ public class NotesRepository {
*
* @param onlyLocalChanges Whether to only push local changes to the server or to also load the whole list of notes from the server.
*/
public synchronized void scheduleSync(Account account, boolean onlyLocalChanges) {
public synchronized void scheduleSync(@Nullable Account account, boolean onlyLocalChanges) {
if (account == null) {
Log.i(TAG, SingleSignOnAccount.class.getSimpleName() + " is null. Is this a local account?");
} else {

View file

@ -20,7 +20,6 @@ import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import it.niedermann.owncloud.notes.persistence.entity.Account;
@ -29,6 +28,7 @@ import it.niedermann.owncloud.notes.persistence.sync.NotesAPI;
import it.niedermann.owncloud.notes.shared.model.DBStatus;
import it.niedermann.owncloud.notes.shared.model.ISyncCallback;
import it.niedermann.owncloud.notes.shared.model.SyncResultStatus;
import it.niedermann.owncloud.notes.shared.util.ApiVersionUtil;
import retrofit2.Response;
import static it.niedermann.owncloud.notes.shared.model.DBStatus.LOCAL_DELETED;
@ -82,7 +82,7 @@ abstract class NotesServerSyncTask extends Thread {
public void run() {
onPreExecute();
notesAPI = ApiProvider.getNotesAPI(context, ssoAccount, localAccount.getPreferredApiVersion());
notesAPI = ApiProvider.getNotesAPI(context, ssoAccount, ApiVersionUtil.getPreferredApiVersion(localAccount.getApiVersion()));
Log.i(TAG, "STARTING SYNCHRONIZATION");
@ -249,19 +249,10 @@ abstract class NotesServerSyncTask extends Thread {
repo.updateETag(localAccount.getId(), localAccount.getETag());
repo.updateModified(localAccount.getId(), localAccount.getModified().getTimeInMillis());
String supportedApiVersions = null;
final String supportedApiVersionsHeader = fetchResponse.getHeaders().get(HEADER_KEY_X_NOTES_API_VERSIONS);
if (supportedApiVersionsHeader != null) {
supportedApiVersions = "[" + Objects.requireNonNull(supportedApiVersionsHeader) + "]";
}
try {
if (repo.updateApiVersion(localAccount.getId(), supportedApiVersions)) {
localAccount.setApiVersion(supportedApiVersions);
}
} catch (Exception e) {
exceptions.add(e);
}
final String newApiVersion = ApiVersionUtil.sanitize(fetchResponse.getHeaders().get(HEADER_KEY_X_NOTES_API_VERSIONS));
localAccount.setApiVersion(newApiVersion);
repo.updateApiVersion(localAccount.getId(), newApiVersion);
Log.d(TAG, "ApiVersion: " + newApiVersion);
return true;
} catch (Throwable t) {
final Throwable cause = t.getCause();

View file

@ -73,31 +73,6 @@ public class Account implements Serializable {
setCapabilities(capabilities);
}
@Nullable
public ApiVersion getPreferredApiVersion() {
// TODO move this logic to NotesClient?
try {
if (apiVersion == null) {
return null;
}
final JSONArray versionsArray = new JSONArray(apiVersion);
final Collection<ApiVersion> supportedApiVersions = new HashSet<>(versionsArray.length());
for (int i = 0; i < versionsArray.length(); i++) {
final ApiVersion parsedApiVersion = ApiVersion.of(versionsArray.getString(i));
for (ApiVersion temp : ApiVersion.SUPPORTED_API_VERSIONS) {
if (temp.equals(parsedApiVersion)) {
supportedApiVersions.add(parsedApiVersion);
break;
}
}
}
return Collections.max(supportedApiVersions);
} catch (JSONException | NoSuchElementException e) {
e.printStackTrace();
return null;
}
}
public void setCapabilities(@NonNull Capabilities capabilities) {
capabilitiesETag = capabilities.getETag();
apiVersion = capabilities.getApiVersion();

View file

@ -41,8 +41,7 @@ public class CapabilitiesDeserializer implements JsonDeserializer<Capabilities>
if (capabilities.has(CAPABILITIES_NOTES)) {
final JsonObject notes = capabilities.getAsJsonObject(CAPABILITIES_NOTES);
if (notes.has(CAPABILITIES_NOTES_API_VERSION)) {
final JsonElement apiVersion = notes.get(CAPABILITIES_NOTES_API_VERSION);
response.setApiVersion(apiVersion.isJsonArray() ? apiVersion.toString() : null);
response.setApiVersion(notes.get(CAPABILITIES_NOTES_API_VERSION).toString());
}
}
if (capabilities.has(CAPABILITIES_THEMING)) {

View file

@ -83,6 +83,9 @@ public class ApiVersion implements Comparable<ApiVersion> {
return 0;
}
/**
* Checks only the <strong>{@link #major}</strong> version.
*/
@Override
public boolean equals(Object o) {
if (this == o) return true;

View file

@ -0,0 +1,107 @@
package it.niedermann.owncloud.notes.shared.util;
import android.text.TextUtils;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import org.json.JSONArray;
import org.json.JSONException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.Objects;
import java.util.stream.Collectors;
import it.niedermann.owncloud.notes.shared.model.ApiVersion;
public class ApiVersionUtil {
private static final String TAG = ApiVersionUtil.class.getSimpleName();
private ApiVersionUtil() {
}
/**
* @return a {@link Collection} of all valid {@link ApiVersion}s which have been found in {@param raw}.
*/
@NonNull
public static Collection<ApiVersion> parse(@Nullable String raw) {
if (TextUtils.isEmpty(raw)) {
return Collections.emptyList();
}
JSONArray a;
try {
a = new JSONArray(raw);
} catch (JSONException e) {
try {
a = new JSONArray("[" + raw + "]");
} catch (JSONException e1) {
return Collections.emptyList();
}
}
final Collection<ApiVersion> result = new ArrayList<>();
for (int i = 0; i < a.length(); i++) {
try {
final ApiVersion version = ApiVersion.of(a.getString(i));
if (version.getMajor() != 0 || version.getMinor() != 0) {
result.add(version);
}
} catch (Exception ignored) {
}
}
return result;
}
/**
* @return a serialized {@link String} of the given {@param apiVersions} or <code>null</code>.
*/
@Nullable
public static String serialize(@Nullable Collection<ApiVersion> apiVersions) {
if (apiVersions == null || apiVersions.isEmpty()) {
return null;
}
return "[" +
apiVersions
.stream()
.filter(Objects::nonNull)
.map(v -> v.getMajor() + "." + v.getMinor())
.collect(Collectors.joining(","))
+ "]";
}
@Nullable
public static String sanitize(@Nullable String raw) {
return serialize(parse(raw));
}
/**
* @return the highest {@link ApiVersion} that is supported by the server according to {@param raw},
* whose major version is also supported by this app (see {@link ApiVersion#SUPPORTED_API_VERSIONS}).
* Returns <code>null</code> if no better version could be found.
*/
@Nullable
public static ApiVersion getPreferredApiVersion(@Nullable String raw) {
return parse(raw)
.stream()
.filter(version -> Arrays.asList(ApiVersion.SUPPORTED_API_VERSIONS).contains(version))
.max((o1, o2) -> {
if (o2.getMajor() > o1.getMajor()) {
return -1;
} else if (o2.getMajor() < o1.getMajor()) {
return 1;
} else if (o2.getMinor() > o1.getMinor()) {
return -1;
} else if (o2.getMinor() < o1.getMinor()) {
return 1;
}
return 0;
})
.orElse(null);
}
}

View file

@ -156,23 +156,29 @@ public class NotesRepositoryTest {
@Test
public void updateApiVersion() {
assertThrows(IllegalArgumentException.class, () -> repo.updateApiVersion(account.getId(), ""));
assertThrows(IllegalArgumentException.class, () -> repo.updateApiVersion(account.getId(), "asdf"));
assertThrows(IllegalArgumentException.class, () -> repo.updateApiVersion(account.getId(), "{}"));
repo.updateApiVersion(account.getId(), "");
assertNull(repo.getAccountById(account.getId()).getApiVersion());
repo.updateApiVersion(account.getId(), "foo");
assertNull(repo.getAccountById(account.getId()).getApiVersion());
repo.updateApiVersion(account.getId(), "{}");
assertNull(repo.getAccountById(account.getId()).getApiVersion());
repo.updateApiVersion(account.getId(), null);
assertNull(repo.getAccountById(account.getId()).getApiVersion());
repo.updateApiVersion(account.getId(), "[]");
assertNull(repo.getAccountById(account.getId()).getApiVersion());
repo.updateApiVersion(account.getId(), "[1.0]");
assertEquals("[1.0]", repo.getAccountById(account.getId()).getApiVersion());
repo.updateApiVersion(account.getId(), "[0.2, 1.0]");
assertEquals("[0.2, 1.0]", repo.getAccountById(account.getId()).getApiVersion());
// TODO is this really indented?
repo.updateApiVersion(account.getId(), "[0.2, abc]");
assertEquals("[0.2, abc]", repo.getAccountById(account.getId()).getApiVersion());
repo.updateApiVersion(account.getId(), "[0.2, 1.0]");
assertEquals("[0.2,1.0]", repo.getAccountById(account.getId()).getApiVersion());
repo.updateApiVersion(account.getId(), "[0.2, foo]");
assertEquals("[0.2]", repo.getAccountById(account.getId()).getApiVersion());
}
@Test

View file

@ -107,7 +107,7 @@ public class CapabilitiesTest {
"}";
final Capabilities capabilities = new CapabilitiesDeserializer().deserialize(JsonParser.parseString(response), null, null);
assertNull(capabilities.getETag());
assertNull(capabilities.getApiVersion());
assertEquals("\"1.0\"", capabilities.getApiVersion());
assertEquals(Color.parseColor("#1E4164"), capabilities.getColor());
assertEquals(Color.parseColor("#ffffff"), capabilities.getTextColor());
}

View file

@ -0,0 +1,227 @@
package it.niedermann.owncloud.notes.shared.util;
import android.os.Build;
import junit.framework.TestCase;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.robolectric.RobolectricTestRunner;
import org.robolectric.annotation.Config;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.Iterator;
import it.niedermann.owncloud.notes.shared.model.ApiVersion;
@RunWith(RobolectricTestRunner.class)
@Config(sdk = {Build.VERSION_CODES.P})
public class ApiVersionUtilTest extends TestCase {
@Test
public void testSanitizeVersionString_invalid_one() {
assertEquals(0, ApiVersionUtil.parse(null).size());
assertEquals(0, ApiVersionUtil.parse("").size());
assertEquals(0, ApiVersionUtil.parse(" ").size());
assertEquals(0, ApiVersionUtil.parse("{}").size());
assertEquals(0, ApiVersionUtil.parse("[]").size());
}
@Test
public void testSanitizeVersionString_valid_one() {
Collection<ApiVersion> result;
ApiVersion current;
result = ApiVersionUtil.parse("[0.2]");
assertEquals(1, result.size());
current = result.iterator().next();
assertEquals(0, current.getMajor());
assertEquals(2, current.getMinor());
result = ApiVersionUtil.parse("[1.0]");
assertEquals(1, result.size());
current = result.iterator().next();
assertEquals(1, current.getMajor());
assertEquals(0, current.getMinor());
result = ApiVersionUtil.parse("[\"0.2\"]");
assertEquals(1, result.size());
current = result.iterator().next();
assertEquals(0, current.getMajor());
assertEquals(2, current.getMinor());
result = ApiVersionUtil.parse("[\"1.0\"]");
assertEquals(1, result.size());
current = result.iterator().next();
assertEquals(1, current.getMajor());
assertEquals(0, current.getMinor());
result = ApiVersionUtil.parse("1.0");
assertEquals(1, result.size());
current = result.iterator().next();
assertEquals(1, current.getMajor());
assertEquals(0, current.getMinor());
}
@Test
public void testSanitizeVersionString_invalid_many() {
Collection<ApiVersion> result;
ApiVersion current;
Iterator<ApiVersion> iterator;
result = ApiVersionUtil.parse("[0.2, foo]");
assertEquals(1, result.size());
iterator = result.iterator();
current = iterator.next();
assertEquals(0, current.getMajor());
assertEquals(2, current.getMinor());
result = ApiVersionUtil.parse("[foo, 1.1]");
assertEquals(1, result.size());
iterator = result.iterator();
current = iterator.next();
assertEquals(1, current.getMajor());
assertEquals(1, current.getMinor());
assertEquals(0, ApiVersionUtil.parse("[foo, bar]").size());
result = ApiVersionUtil.parse("[, 1.1]");
assertEquals(1, result.size());
iterator = result.iterator();
current = iterator.next();
assertEquals(1, current.getMajor());
assertEquals(1, current.getMinor());
result = ApiVersionUtil.parse("[1.1, ?]");
assertEquals(1, result.size());
iterator = result.iterator();
current = iterator.next();
assertEquals(1, current.getMajor());
assertEquals(1, current.getMinor());
}
@Test
public void testSanitizeVersionString_valid_many() {
Collection<ApiVersion> result;
ApiVersion current;
Iterator<ApiVersion> iterator;
result = ApiVersionUtil.parse("[0.2, 1.0]");
assertEquals(2, result.size());
iterator = result.iterator();
current = iterator.next();
assertEquals(0, current.getMajor());
assertEquals(2, current.getMinor());
current = iterator.next();
assertEquals(1, current.getMajor());
assertEquals(0, current.getMinor());
result = ApiVersionUtil.parse("[\"0.2\", \"1.0\"]");
assertEquals(2, result.size());
iterator = result.iterator();
current = iterator.next();
assertEquals(0, current.getMajor());
assertEquals(2, current.getMinor());
current = iterator.next();
assertEquals(1, current.getMajor());
assertEquals(0, current.getMinor());
result = ApiVersionUtil.parse("[0.2,1.0]");
assertEquals(2, result.size());
iterator = result.iterator();
current = iterator.next();
assertEquals(0, current.getMajor());
assertEquals(2, current.getMinor());
current = iterator.next();
assertEquals(1, current.getMajor());
assertEquals(0, current.getMinor());
result = ApiVersionUtil.parse("[\"0.2\",\"1.0\"]");
assertEquals(2, result.size());
iterator = result.iterator();
current = iterator.next();
assertEquals(0, current.getMajor());
assertEquals(2, current.getMinor());
current = iterator.next();
assertEquals(1, current.getMajor());
assertEquals(0, current.getMinor());
result = ApiVersionUtil.parse("[0.2, \"1.0\"]");
assertEquals(2, result.size());
iterator = result.iterator();
current = iterator.next();
assertEquals(0, current.getMajor());
assertEquals(2, current.getMinor());
current = iterator.next();
assertEquals(1, current.getMajor());
assertEquals(0, current.getMinor());
result = ApiVersionUtil.parse("[0.2,\"1.0\"]");
assertEquals(2, result.size());
iterator = result.iterator();
current = iterator.next();
assertEquals(0, current.getMajor());
assertEquals(2, current.getMinor());
current = iterator.next();
assertEquals(1, current.getMajor());
assertEquals(0, current.getMinor());
}
@Test
public void testSerialize() {
assertNull(ApiVersionUtil.serialize(null));
assertNull(ApiVersionUtil.serialize(Collections.emptyList()));
assertEquals("[0.2]", ApiVersionUtil.serialize(Collections.singleton(ApiVersion.API_VERSION_0_2)));
assertEquals("[1.0]", ApiVersionUtil.serialize(Collections.singleton(ApiVersion.API_VERSION_1_0)));
assertEquals("[1.0]", ApiVersionUtil.serialize(Arrays.asList(ApiVersion.API_VERSION_1_0, null)));
assertEquals("[1.0]", ApiVersionUtil.serialize(Arrays.asList(null, ApiVersion.API_VERSION_1_0)));
assertEquals("[0.2,1.0]", ApiVersionUtil.serialize(Arrays.asList(ApiVersion.API_VERSION_0_2, ApiVersion.API_VERSION_1_0)));
// TODO sure...?
assertEquals("[1.0,1.0]", ApiVersionUtil.serialize(Arrays.asList(ApiVersion.API_VERSION_1_0, ApiVersion.API_VERSION_1_0)));
}
@SuppressWarnings("ConstantConditions")
@Test
public void testGetPreferredApiVersion() {
assertNull(ApiVersionUtil.getPreferredApiVersion(null));
assertNull(ApiVersionUtil.getPreferredApiVersion(""));
assertNull(ApiVersionUtil.getPreferredApiVersion("[]"));
assertNull(ApiVersionUtil.getPreferredApiVersion("foo"));
ApiVersion result;
result = ApiVersionUtil.getPreferredApiVersion("[0.2]");
assertEquals(0, result.getMajor());
assertEquals(2, result.getMinor());
result = ApiVersionUtil.getPreferredApiVersion("[1.1]");
assertEquals(1, result.getMajor());
assertEquals(1, result.getMinor());
result = ApiVersionUtil.getPreferredApiVersion("[0.2,1.1]");
assertEquals(1, result.getMajor());
assertEquals(1, result.getMinor());
result = ApiVersionUtil.getPreferredApiVersion("[1.1,0.2]");
assertEquals(1, result.getMajor());
assertEquals(1, result.getMinor());
result = ApiVersionUtil.getPreferredApiVersion("[10.0,1.1,1.0,0.2]");
assertEquals(1, result.getMajor());
assertEquals(1, result.getMinor());
result = ApiVersionUtil.getPreferredApiVersion("[1.1,1.5,1.0]");
assertEquals(1, result.getMajor());
assertEquals(5, result.getMinor());
result = ApiVersionUtil.getPreferredApiVersion("[1.1,,foo,1.0]");
assertEquals(1, result.getMajor());
assertEquals(1, result.getMinor());
}
}