1 /*
2  * Copyright 2021 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 
17 package androidx.appsearch.app;
18 
19 import android.os.Parcel;
20 import android.os.Parcelable;
21 
22 import androidx.annotation.NonNull;
23 import androidx.annotation.Nullable;
24 import androidx.annotation.RestrictTo;
25 import androidx.appsearch.annotation.CanIgnoreReturnValue;
26 import androidx.appsearch.flags.FlaggedApi;
27 import androidx.appsearch.flags.Flags;
28 import androidx.appsearch.safeparcel.AbstractSafeParcelable;
29 import androidx.appsearch.safeparcel.SafeParcelable;
30 import androidx.appsearch.safeparcel.stub.StubCreators.MigrationFailureCreator;
31 import androidx.appsearch.safeparcel.stub.StubCreators.SetSchemaResponseCreator;
32 import androidx.collection.ArraySet;
33 import androidx.core.util.Preconditions;
34 
35 import java.util.ArrayList;
36 import java.util.Collection;
37 import java.util.Collections;
38 import java.util.List;
39 import java.util.Set;
40 
41 /** The response class of {@link AppSearchSession#setSchemaAsync} */
42 @SafeParcelable.Class(creator = "SetSchemaResponseCreator")
43 // TODO(b/384721898): Switch to JSpecify annotations
44 @SuppressWarnings({"HiddenSuperclass", "JSpecifyNullness"})
45 public final class SetSchemaResponse extends AbstractSafeParcelable {
46     @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
47     @FlaggedApi(Flags.FLAG_ENABLE_SAFE_PARCELABLE_2)
48     public static final @NonNull Parcelable.Creator<SetSchemaResponse> CREATOR =
49             new SetSchemaResponseCreator();
50 
51     @Field(id = 1)
52     final List<String> mDeletedTypes;
53     @Field(id = 2)
54     final List<String> mIncompatibleTypes;
55     @Field(id = 3)
56     final List<String> mMigratedTypes;
57 
58     /**
59      * The migrationFailures won't be saved as a SafeParcelable field. Since:
60      * <ul>
61      *     <li>{@link MigrationFailure} is generated in {@link AppSearchSession} which will be
62      *         the SDK side in platform. We don't need to pass it from service side via binder as
63      *         a part of {@link SetSchemaResponse}.
64      *     <li>Writing multiple {@link MigrationFailure}s to SafeParcelable in {@link Builder} and
65      *     then back in constructor will be a huge waste.
66      * </ul>
67      */
68     private final List<MigrationFailure> mMigrationFailures;
69 
70     /** Cache of the inflated deleted schema types. Comes from inflating mDeletedTypes at first use
71      */
72     private @Nullable Set<String> mDeletedTypesCached;
73 
74     /** Cache of the inflated migrated schema types. Comes from inflating mMigratedTypes at first
75      *  use.
76      */
77     private @Nullable Set<String> mMigratedTypesCached;
78 
79     /**
80      * Cache of the inflated incompatible schema types. Comes from inflating mIncompatibleTypes at
81      * first use.
82      */
83     private @Nullable Set<String> mIncompatibleTypesCached;
84 
85     @Constructor
SetSchemaResponse( @aramid = 1) @onNull List<String> deletedTypes, @Param(id = 2) @NonNull List<String> incompatibleTypes, @Param(id = 3) @NonNull List<String> migratedTypes)86     SetSchemaResponse(
87             @Param(id = 1) @NonNull List<String> deletedTypes,
88             @Param(id = 2) @NonNull List<String> incompatibleTypes,
89             @Param(id = 3) @NonNull List<String> migratedTypes) {
90         mDeletedTypes = deletedTypes;
91         mIncompatibleTypes = incompatibleTypes;
92         mMigratedTypes = migratedTypes;
93         mMigrationFailures = Collections.emptyList();
94     }
95 
SetSchemaResponse( @onNull List<String> deletedTypes, @NonNull List<String> incompatibleTypes, @NonNull List<String> migratedTypes, @NonNull List<MigrationFailure> migrationFailures)96     SetSchemaResponse(
97             @NonNull List<String> deletedTypes,
98             @NonNull List<String> incompatibleTypes,
99             @NonNull List<String> migratedTypes,
100             @NonNull List<MigrationFailure> migrationFailures) {
101         mDeletedTypes = deletedTypes;
102         mIncompatibleTypes = incompatibleTypes;
103         mMigratedTypes = migratedTypes;
104         mMigrationFailures = Preconditions.checkNotNull(migrationFailures);
105     }
106 
107     /**
108      * Returns a {@link List} of all failed {@link MigrationFailure}.
109      *
110      * <p>A {@link MigrationFailure} will be generated if the system trying to save a post-migrated
111      * {@link GenericDocument} but fail.
112      *
113      * <p>{@link MigrationFailure} contains the namespace, id and schemaType of the post-migrated
114      * {@link GenericDocument} and the error reason. Mostly it will be mismatch the schema it
115      * migrated to.
116      */
getMigrationFailures()117     public @NonNull List<MigrationFailure> getMigrationFailures() {
118         return Collections.unmodifiableList(mMigrationFailures);
119     }
120 
121     /**
122      * Returns a {@link Set} of deleted schema types.
123      *
124      * <p>A "deleted" type is a schema type that was previously a part of the database schema but
125      * was not present in the {@link SetSchemaRequest} object provided in the
126      * {@link AppSearchSession#setSchemaAsync} call.
127      *
128      * <p>Documents for a deleted type are removed from the database.
129      */
getDeletedTypes()130     public @NonNull Set<String> getDeletedTypes() {
131         if (mDeletedTypesCached == null) {
132             mDeletedTypesCached = new ArraySet<>(Preconditions.checkNotNull(mDeletedTypes));
133         }
134         return Collections.unmodifiableSet(mDeletedTypesCached);
135     }
136 
137     /**
138      * Returns a {@link Set} of schema type that were migrated by the
139      * {@link AppSearchSession#setSchemaAsync} call.
140      *
141      * <p> A "migrated" type is a schema type that has triggered a {@link Migrator} instance to
142      * migrate documents of the schema type to another schema type, or to another version of the
143      * schema type.
144      *
145      * <p>If a document fails to be migrated, a {@link MigrationFailure} will be generated
146      * for that document.
147      *
148      * @see Migrator
149      */
getMigratedTypes()150     public @NonNull Set<String> getMigratedTypes() {
151         if (mMigratedTypesCached == null) {
152             mMigratedTypesCached = new ArraySet<>(Preconditions.checkNotNull(mMigratedTypes));
153         }
154         return Collections.unmodifiableSet(mMigratedTypesCached);
155     }
156 
157     /**
158      * Returns a {@link Set} of schema type whose new definitions set in the
159      * {@link AppSearchSession#setSchemaAsync} call were incompatible with the pre-existing schema.
160      *
161      * <p>If a {@link Migrator} is provided for this type and the migration is success triggered.
162      * The type will also appear in {@link #getMigratedTypes()}.
163      *
164      * @see SetSchemaRequest
165      * @see AppSearchSession#setSchemaAsync
166      * @see SetSchemaRequest.Builder#setForceOverride
167      */
getIncompatibleTypes()168     public @NonNull Set<String> getIncompatibleTypes() {
169         if (mIncompatibleTypesCached == null) {
170             mIncompatibleTypesCached =
171                     new ArraySet<>(Preconditions.checkNotNull(mIncompatibleTypes));
172         }
173         return Collections.unmodifiableSet(mIncompatibleTypesCached);
174     }
175 
176     /** Builder for {@link SetSchemaResponse} objects. */
177     public static final class Builder {
178         private List<MigrationFailure> mMigrationFailures = new ArrayList<>();
179         private ArrayList<String> mDeletedTypes = new ArrayList<>();
180         private ArrayList<String> mMigratedTypes = new ArrayList<>();
181         private ArrayList<String> mIncompatibleTypes = new ArrayList<>();
182         private boolean mBuilt = false;
183 
184         /**
185          * Creates a new {@link SetSchemaResponse.Builder} from the given SetSchemaResponse.
186          *
187          * @exportToFramework:hide
188          */
189         @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
Builder(@onNull SetSchemaResponse setSchemaResponse)190         public Builder(@NonNull SetSchemaResponse setSchemaResponse) {
191             Preconditions.checkNotNull(setSchemaResponse);
192             mDeletedTypes.addAll(setSchemaResponse.getDeletedTypes());
193             mIncompatibleTypes.addAll(setSchemaResponse.getIncompatibleTypes());
194             mMigratedTypes.addAll(setSchemaResponse.getMigratedTypes());
195             mMigrationFailures.addAll(setSchemaResponse.getMigrationFailures());
196         }
197 
198         /** Create a {@link Builder} object} */
Builder()199         public Builder() {}
200 
201         /**  Adds {@link MigrationFailure}s to the list of migration failures. */
202         @CanIgnoreReturnValue
addMigrationFailures( @onNull Collection<MigrationFailure> migrationFailures)203         public @NonNull Builder addMigrationFailures(
204                 @NonNull Collection<MigrationFailure> migrationFailures) {
205             Preconditions.checkNotNull(migrationFailures);
206             resetIfBuilt();
207             mMigrationFailures.addAll(migrationFailures);
208             return this;
209         }
210 
211         /**  Adds a {@link MigrationFailure} to the list of migration failures. */
212         @CanIgnoreReturnValue
addMigrationFailure(@onNull MigrationFailure migrationFailure)213         public @NonNull Builder addMigrationFailure(@NonNull MigrationFailure migrationFailure) {
214             Preconditions.checkNotNull(migrationFailure);
215             resetIfBuilt();
216             mMigrationFailures.add(migrationFailure);
217             return this;
218         }
219 
220         /**  Adds {@code deletedTypes} to the list of deleted schema types. */
221         @CanIgnoreReturnValue
addDeletedTypes(@onNull Collection<String> deletedTypes)222         public @NonNull Builder addDeletedTypes(@NonNull Collection<String> deletedTypes) {
223             Preconditions.checkNotNull(deletedTypes);
224             resetIfBuilt();
225             mDeletedTypes.addAll(deletedTypes);
226             return this;
227         }
228 
229         /**  Adds one {@code deletedType} to the list of deleted schema types. */
230         @CanIgnoreReturnValue
addDeletedType(@onNull String deletedType)231         public @NonNull Builder addDeletedType(@NonNull String deletedType) {
232             Preconditions.checkNotNull(deletedType);
233             resetIfBuilt();
234             mDeletedTypes.add(deletedType);
235             return this;
236         }
237 
238         /**  Adds {@code incompatibleTypes} to the list of incompatible schema types. */
239         @CanIgnoreReturnValue
addIncompatibleTypes( @onNull Collection<String> incompatibleTypes)240         public @NonNull Builder addIncompatibleTypes(
241                 @NonNull Collection<String> incompatibleTypes) {
242             Preconditions.checkNotNull(incompatibleTypes);
243             resetIfBuilt();
244             mIncompatibleTypes.addAll(incompatibleTypes);
245             return this;
246         }
247 
248         /**  Adds one {@code incompatibleType} to the list of incompatible schema types. */
249         @CanIgnoreReturnValue
addIncompatibleType(@onNull String incompatibleType)250         public @NonNull Builder addIncompatibleType(@NonNull String incompatibleType) {
251             Preconditions.checkNotNull(incompatibleType);
252             resetIfBuilt();
253             mIncompatibleTypes.add(incompatibleType);
254             return this;
255         }
256 
257         /**  Adds {@code migratedTypes} to the list of migrated schema types. */
258         @CanIgnoreReturnValue
addMigratedTypes(@onNull Collection<String> migratedTypes)259         public @NonNull Builder addMigratedTypes(@NonNull Collection<String> migratedTypes) {
260             Preconditions.checkNotNull(migratedTypes);
261             resetIfBuilt();
262             mMigratedTypes.addAll(migratedTypes);
263             return this;
264         }
265 
266         /**  Adds one {@code migratedType} to the list of migrated schema types. */
267         @CanIgnoreReturnValue
addMigratedType(@onNull String migratedType)268         public @NonNull Builder addMigratedType(@NonNull String migratedType) {
269             Preconditions.checkNotNull(migratedType);
270             resetIfBuilt();
271             mMigratedTypes.add(migratedType);
272             return this;
273         }
274 
275         /** Builds a {@link SetSchemaResponse} object. */
build()276         public @NonNull SetSchemaResponse build() {
277             mBuilt = true;
278             // Avoid converting the potential thousands of MigrationFailures to Pracelable and
279             // back just for put in bundle. In platform, we should set MigrationFailures in
280             // AppSearchSession after we pass SetSchemaResponse via binder.
281             return new SetSchemaResponse(
282                     mDeletedTypes,
283                     mIncompatibleTypes,
284                     mMigratedTypes,
285                     mMigrationFailures);
286         }
287 
resetIfBuilt()288         private void resetIfBuilt() {
289             if (mBuilt) {
290                 mMigrationFailures = new ArrayList<>(mMigrationFailures);
291                 mDeletedTypes = new ArrayList<>(mDeletedTypes);
292                 mMigratedTypes = new ArrayList<>(mMigratedTypes);
293                 mIncompatibleTypes = new ArrayList<>(mIncompatibleTypes);
294                 mBuilt = false;
295             }
296         }
297     }
298 
299     @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
300     @FlaggedApi(Flags.FLAG_ENABLE_SAFE_PARCELABLE_2)
301     @Override
writeToParcel(@onNull Parcel dest, int flags)302     public void writeToParcel(@NonNull Parcel dest, int flags) {
303         SetSchemaResponseCreator.writeToParcel(this, dest, flags);
304     }
305 
306     /**
307      * The class represents a post-migrated {@link GenericDocument} that failed to be saved by
308      * {@link AppSearchSession#setSchemaAsync}.
309      */
310     @SafeParcelable.Class(creator = "MigrationFailureCreator")
311     @SuppressWarnings("HiddenSuperclass")
312     public static class MigrationFailure extends AbstractSafeParcelable {
313         @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
314         @FlaggedApi(Flags.FLAG_ENABLE_SAFE_PARCELABLE_2)
315         public static final @NonNull Parcelable.Creator<MigrationFailure> CREATOR =
316                 new MigrationFailureCreator();
317 
318         @Field(id = 1, getter = "getNamespace")
319         private final String mNamespace;
320         @Field(id = 2, getter = "getDocumentId")
321         private final String mDocumentId;
322         @Field(id = 3, getter = "getSchemaType")
323         private final String mSchemaType;
324         @Field(id = 4)
325 final @Nullable String mErrorMessage;
326         @Field(id = 5)
327         final int mResultCode;
328 
329         @Constructor
MigrationFailure( @aramid = 1) @onNull String namespace, @Param(id = 2) @NonNull String documentId, @Param(id = 3) @NonNull String schemaType, @Param(id = 4) @Nullable String errorMessage, @Param(id = 5) int resultCode)330         MigrationFailure(
331                 @Param(id = 1) @NonNull String namespace,
332                 @Param(id = 2) @NonNull String documentId,
333                 @Param(id = 3) @NonNull String schemaType,
334                 @Param(id = 4) @Nullable String errorMessage,
335                 @Param(id = 5) int resultCode) {
336             mNamespace = namespace;
337             mDocumentId = documentId;
338             mSchemaType = schemaType;
339             mErrorMessage = errorMessage;
340             mResultCode = resultCode;
341         }
342 
343         /**
344          * Constructs a new {@link MigrationFailure}.
345          *
346          * @param namespace    The namespace of the document which failed to be migrated.
347          * @param documentId   The id of the document which failed to be migrated.
348          * @param schemaType   The type of the document which failed to be migrated.
349          * @param failedResult The reason why the document failed to be indexed.
350          * @throws IllegalArgumentException if the provided {@code failedResult} was not a failure.
351          */
MigrationFailure( @onNull String namespace, @NonNull String documentId, @NonNull String schemaType, @NonNull AppSearchResult<?> failedResult)352         public MigrationFailure(
353                 @NonNull String namespace,
354                 @NonNull String documentId,
355                 @NonNull String schemaType,
356                 @NonNull AppSearchResult<?> failedResult) {
357             mNamespace = namespace;
358             mDocumentId = documentId;
359             mSchemaType = schemaType;
360 
361             Preconditions.checkNotNull(failedResult);
362             Preconditions.checkArgument(
363                     !failedResult.isSuccess(), "failedResult was actually successful");
364             mErrorMessage = failedResult.getErrorMessage();
365             mResultCode = failedResult.getResultCode();
366         }
367 
368         /** Returns the namespace of the {@link GenericDocument} that failed to be migrated. */
getNamespace()369         public @NonNull String getNamespace() {
370             return mNamespace;
371         }
372 
373         /** Returns the id of the {@link GenericDocument} that failed to be migrated. */
getDocumentId()374         public @NonNull String getDocumentId() {
375             return mDocumentId;
376         }
377 
378         /** Returns the schema type of the {@link GenericDocument} that failed to be migrated. */
getSchemaType()379         public @NonNull String getSchemaType() {
380             return mSchemaType;
381         }
382 
383         /**
384          * Returns the {@link AppSearchResult} that indicates why the
385          * post-migration {@link GenericDocument} failed to be indexed.
386          */
getAppSearchResult()387         public @NonNull AppSearchResult<Void> getAppSearchResult() {
388             return AppSearchResult.newFailedResult(mResultCode, mErrorMessage);
389         }
390 
391         @Override
toString()392         public @NonNull String toString() {
393             return "MigrationFailure { schemaType: " + getSchemaType() + ", namespace: "
394                     + getNamespace() + ", documentId: " + getDocumentId() + ", appSearchResult: "
395                     + getAppSearchResult().toString() + "}";
396         }
397 
398         @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
399         @FlaggedApi(Flags.FLAG_ENABLE_SAFE_PARCELABLE_2)
400         @Override
writeToParcel(@onNull Parcel dest, int flags)401         public void writeToParcel(@NonNull Parcel dest, int flags) {
402             MigrationFailureCreator.writeToParcel(this, dest, flags);
403         }
404     }
405 }
406