1 /*
2  * Copyright 2020 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 com.android.server.appsearch.external.localstorage.converter;
18 
19 import android.annotation.NonNull;
20 import android.app.appsearch.AppSearchSchema;
21 import android.util.Log;
22 
23 import com.google.android.icing.proto.DocumentIndexingConfig;
24 import com.google.android.icing.proto.EmbeddingIndexingConfig;
25 import com.google.android.icing.proto.IntegerIndexingConfig;
26 import com.google.android.icing.proto.JoinableConfig;
27 import com.google.android.icing.proto.PropertyConfigProto;
28 import com.google.android.icing.proto.SchemaTypeConfigProto;
29 import com.google.android.icing.proto.SchemaTypeConfigProtoOrBuilder;
30 import com.google.android.icing.proto.StringIndexingConfig;
31 import com.google.android.icing.proto.TermMatchType;
32 
33 import java.util.List;
34 import java.util.Objects;
35 
36 /**
37  * Translates an {@link AppSearchSchema} into a {@link SchemaTypeConfigProto}.
38  *
39  * @hide
40  */
41 public final class SchemaToProtoConverter {
42     private static final String TAG = "AppSearchSchemaToProtoC";
43 
SchemaToProtoConverter()44     private SchemaToProtoConverter() {}
45 
46     /**
47      * Converts an {@link android.app.appsearch.AppSearchSchema} into a {@link
48      * SchemaTypeConfigProto}.
49      */
50     // TODO(b/284356266): Consider handling addition of schema name prefixes in this function.
51     @NonNull
toSchemaTypeConfigProto( @onNull AppSearchSchema schema, int version)52     public static SchemaTypeConfigProto toSchemaTypeConfigProto(
53             @NonNull AppSearchSchema schema, int version) {
54         Objects.requireNonNull(schema);
55         SchemaTypeConfigProto.Builder protoBuilder =
56                 SchemaTypeConfigProto.newBuilder()
57                         .setSchemaType(schema.getSchemaType())
58                         .setDescription(schema.getDescription())
59                         .setVersion(version);
60         List<AppSearchSchema.PropertyConfig> properties = schema.getProperties();
61         for (int i = 0; i < properties.size(); i++) {
62             PropertyConfigProto propertyProto = toPropertyConfigProto(properties.get(i));
63             protoBuilder.addProperties(propertyProto);
64         }
65         protoBuilder.addAllParentTypes(schema.getParentTypes());
66         return protoBuilder.build();
67     }
68 
69     @NonNull
toPropertyConfigProto( @onNull AppSearchSchema.PropertyConfig property)70     private static PropertyConfigProto toPropertyConfigProto(
71             @NonNull AppSearchSchema.PropertyConfig property) {
72         Objects.requireNonNull(property);
73         PropertyConfigProto.Builder builder =
74                 PropertyConfigProto.newBuilder()
75                         .setPropertyName(property.getName())
76                         .setDescription(property.getDescription());
77 
78         // Set dataType
79         @AppSearchSchema.PropertyConfig.DataType int dataType = property.getDataType();
80         PropertyConfigProto.DataType.Code dataTypeProto =
81                 PropertyConfigProto.DataType.Code.forNumber(dataType);
82         if (dataTypeProto == null) {
83             throw new IllegalArgumentException("Invalid dataType: " + dataType);
84         }
85         builder.setDataType(dataTypeProto);
86 
87         // Set cardinality
88         @AppSearchSchema.PropertyConfig.Cardinality int cardinality = property.getCardinality();
89         PropertyConfigProto.Cardinality.Code cardinalityProto =
90                 PropertyConfigProto.Cardinality.Code.forNumber(cardinality);
91         if (cardinalityProto == null) {
92             throw new IllegalArgumentException("Invalid cardinality: " + dataType);
93         }
94         builder.setCardinality(cardinalityProto);
95 
96         if (property instanceof AppSearchSchema.StringPropertyConfig) {
97             AppSearchSchema.StringPropertyConfig stringProperty =
98                     (AppSearchSchema.StringPropertyConfig) property;
99 
100             // Set JoinableConfig only if it is joinable (i.e. joinableValueType is not NONE).
101             if (stringProperty.getJoinableValueType()
102                     != AppSearchSchema.StringPropertyConfig.JOINABLE_VALUE_TYPE_NONE) {
103                 JoinableConfig joinableConfig =
104                         JoinableConfig.newBuilder()
105                                 .setValueType(
106                                         convertJoinableValueTypeToProto(
107                                                 stringProperty.getJoinableValueType()))
108                                 .build();
109                 builder.setJoinableConfig(joinableConfig);
110             }
111 
112             StringIndexingConfig stringIndexingConfig =
113                     StringIndexingConfig.newBuilder()
114                             .setTermMatchType(
115                                     convertTermMatchTypeToProto(stringProperty.getIndexingType()))
116                             .setTokenizerType(
117                                     convertTokenizerTypeToProto(stringProperty.getTokenizerType()))
118                             .build();
119             builder.setStringIndexingConfig(stringIndexingConfig);
120 
121         } else if (property instanceof AppSearchSchema.DocumentPropertyConfig) {
122             AppSearchSchema.DocumentPropertyConfig documentProperty =
123                     (AppSearchSchema.DocumentPropertyConfig) property;
124             builder.setSchemaType(documentProperty.getSchemaType())
125                     .setDocumentIndexingConfig(
126                             DocumentIndexingConfig.newBuilder()
127                                     .setIndexNestedProperties(
128                                             documentProperty.shouldIndexNestedProperties())
129                                     .addAllIndexableNestedPropertiesList(
130                                             documentProperty.getIndexableNestedProperties()));
131         } else if (property instanceof AppSearchSchema.LongPropertyConfig) {
132             AppSearchSchema.LongPropertyConfig longProperty =
133                     (AppSearchSchema.LongPropertyConfig) property;
134             // Set integer indexing config only if it is indexable (i.e. not INDEXING_TYPE_NONE).
135             if (longProperty.getIndexingType()
136                     != AppSearchSchema.LongPropertyConfig.INDEXING_TYPE_NONE) {
137                 IntegerIndexingConfig integerIndexingConfig =
138                         IntegerIndexingConfig.newBuilder()
139                                 .setNumericMatchType(
140                                         convertNumericMatchTypeToProto(
141                                                 longProperty.getIndexingType()))
142                                 .build();
143                 builder.setIntegerIndexingConfig(integerIndexingConfig);
144             }
145         } else if (property instanceof AppSearchSchema.EmbeddingPropertyConfig) {
146             AppSearchSchema.EmbeddingPropertyConfig embeddingProperty =
147                     (AppSearchSchema.EmbeddingPropertyConfig) property;
148             // Set embedding indexing config only if it is indexable (i.e. not INDEXING_TYPE_NONE).
149             // Non-indexable embedding property only requires to builder.setDataType, without the
150             // need to set an EmbeddingIndexingConfig.
151             if (embeddingProperty.getIndexingType()
152                     != AppSearchSchema.EmbeddingPropertyConfig.INDEXING_TYPE_NONE) {
153                 EmbeddingIndexingConfig embeddingIndexingConfig =
154                         EmbeddingIndexingConfig.newBuilder()
155                                 .setEmbeddingIndexingType(
156                                         convertEmbeddingIndexingTypeToProto(
157                                                 embeddingProperty.getIndexingType()))
158                                 .build();
159                 builder.setEmbeddingIndexingConfig(embeddingIndexingConfig);
160             }
161         }
162         return builder.build();
163     }
164 
165     /**
166      * Converts a {@link SchemaTypeConfigProto} into an {@link
167      * android.app.appsearch.AppSearchSchema}.
168      */
169     // TODO(b/284356266): Consider handling removal of schema name prefixes in this function.
170     @NonNull
toAppSearchSchema(@onNull SchemaTypeConfigProtoOrBuilder proto)171     public static AppSearchSchema toAppSearchSchema(@NonNull SchemaTypeConfigProtoOrBuilder proto) {
172         Objects.requireNonNull(proto);
173         AppSearchSchema.Builder builder = new AppSearchSchema.Builder(proto.getSchemaType());
174         builder.setDescription(proto.getDescription());
175         List<PropertyConfigProto> properties = proto.getPropertiesList();
176         for (int i = 0; i < properties.size(); i++) {
177             AppSearchSchema.PropertyConfig propertyConfig = toPropertyConfig(properties.get(i));
178             builder.addProperty(propertyConfig);
179         }
180         List<String> parentTypes = proto.getParentTypesList();
181         for (int i = 0; i < parentTypes.size(); i++) {
182             builder.addParentType(parentTypes.get(i));
183         }
184         return builder.build();
185     }
186 
187     @NonNull
toPropertyConfig( @onNull PropertyConfigProto proto)188     private static AppSearchSchema.PropertyConfig toPropertyConfig(
189             @NonNull PropertyConfigProto proto) {
190         Objects.requireNonNull(proto);
191         switch (proto.getDataType()) {
192             case STRING:
193                 return toStringPropertyConfig(proto);
194             case INT64:
195                 return toLongPropertyConfig(proto);
196             case DOUBLE:
197                 return new AppSearchSchema.DoublePropertyConfig.Builder(proto.getPropertyName())
198                         .setDescription(proto.getDescription())
199                         .setCardinality(proto.getCardinality().getNumber())
200                         .build();
201             case BOOLEAN:
202                 return new AppSearchSchema.BooleanPropertyConfig.Builder(proto.getPropertyName())
203                         .setDescription(proto.getDescription())
204                         .setCardinality(proto.getCardinality().getNumber())
205                         .build();
206             case BYTES:
207                 return new AppSearchSchema.BytesPropertyConfig.Builder(proto.getPropertyName())
208                         .setDescription(proto.getDescription())
209                         .setCardinality(proto.getCardinality().getNumber())
210                         .build();
211             case DOCUMENT:
212                 return toDocumentPropertyConfig(proto);
213             case VECTOR:
214                 return toEmbeddingPropertyConfig(proto);
215             default:
216                 throw new IllegalArgumentException(
217                         "Invalid dataType code: " + proto.getDataType().getNumber());
218         }
219     }
220 
221     @NonNull
toStringPropertyConfig( @onNull PropertyConfigProto proto)222     private static AppSearchSchema.StringPropertyConfig toStringPropertyConfig(
223             @NonNull PropertyConfigProto proto) {
224         AppSearchSchema.StringPropertyConfig.Builder builder =
225                 new AppSearchSchema.StringPropertyConfig.Builder(proto.getPropertyName())
226                         .setDescription(proto.getDescription())
227                         .setCardinality(proto.getCardinality().getNumber())
228                         .setJoinableValueType(
229                                 convertJoinableValueTypeFromProto(
230                                         proto.getJoinableConfig().getValueType()))
231                         .setTokenizerType(
232                                 proto.getStringIndexingConfig().getTokenizerType().getNumber());
233 
234         // Set indexingType
235         TermMatchType.Code termMatchTypeProto = proto.getStringIndexingConfig().getTermMatchType();
236         builder.setIndexingType(convertTermMatchTypeFromProto(termMatchTypeProto));
237 
238         return builder.build();
239     }
240 
241     @NonNull
toDocumentPropertyConfig( @onNull PropertyConfigProto proto)242     private static AppSearchSchema.DocumentPropertyConfig toDocumentPropertyConfig(
243             @NonNull PropertyConfigProto proto) {
244         AppSearchSchema.DocumentPropertyConfig.Builder builder =
245                 new AppSearchSchema.DocumentPropertyConfig.Builder(
246                                 proto.getPropertyName(), proto.getSchemaType())
247                         .setDescription(proto.getDescription())
248                         .setCardinality(proto.getCardinality().getNumber())
249                         .setShouldIndexNestedProperties(
250                                 proto.getDocumentIndexingConfig().getIndexNestedProperties());
251         builder.addIndexableNestedProperties(
252                 proto.getDocumentIndexingConfig().getIndexableNestedPropertiesListList());
253         return builder.build();
254     }
255 
256     @NonNull
toLongPropertyConfig( @onNull PropertyConfigProto proto)257     private static AppSearchSchema.LongPropertyConfig toLongPropertyConfig(
258             @NonNull PropertyConfigProto proto) {
259         AppSearchSchema.LongPropertyConfig.Builder builder =
260                 new AppSearchSchema.LongPropertyConfig.Builder(proto.getPropertyName())
261                         .setDescription(proto.getDescription())
262                         .setCardinality(proto.getCardinality().getNumber());
263 
264         // Set indexingType
265         IntegerIndexingConfig.NumericMatchType.Code numericMatchTypeProto =
266                 proto.getIntegerIndexingConfig().getNumericMatchType();
267         builder.setIndexingType(convertNumericMatchTypeFromProto(numericMatchTypeProto));
268 
269         return builder.build();
270     }
271 
272     @NonNull
toEmbeddingPropertyConfig( @onNull PropertyConfigProto proto)273     private static AppSearchSchema.EmbeddingPropertyConfig toEmbeddingPropertyConfig(
274             @NonNull PropertyConfigProto proto) {
275         AppSearchSchema.EmbeddingPropertyConfig.Builder builder =
276                 new AppSearchSchema.EmbeddingPropertyConfig.Builder(proto.getPropertyName())
277                         .setCardinality(proto.getCardinality().getNumber());
278 
279         // Set indexingType
280         EmbeddingIndexingConfig.EmbeddingIndexingType.Code embeddingIndexingType =
281                 proto.getEmbeddingIndexingConfig().getEmbeddingIndexingType();
282         builder.setIndexingType(convertEmbeddingIndexingTypeFromProto(embeddingIndexingType));
283 
284         return builder.build();
285     }
286 
287     @NonNull
convertJoinableValueTypeToProto( @ppSearchSchema.StringPropertyConfig.JoinableValueType int joinableValueType)288     private static JoinableConfig.ValueType.Code convertJoinableValueTypeToProto(
289             @AppSearchSchema.StringPropertyConfig.JoinableValueType int joinableValueType) {
290         switch (joinableValueType) {
291             case AppSearchSchema.StringPropertyConfig.JOINABLE_VALUE_TYPE_NONE:
292                 return JoinableConfig.ValueType.Code.NONE;
293             case AppSearchSchema.StringPropertyConfig.JOINABLE_VALUE_TYPE_QUALIFIED_ID:
294                 return JoinableConfig.ValueType.Code.QUALIFIED_ID;
295             default:
296                 throw new IllegalArgumentException(
297                         "Invalid joinableValueType: " + joinableValueType);
298         }
299     }
300 
301     @AppSearchSchema.StringPropertyConfig.JoinableValueType
convertJoinableValueTypeFromProto( @onNull JoinableConfig.ValueType.Code joinableValueType)302     private static int convertJoinableValueTypeFromProto(
303             @NonNull JoinableConfig.ValueType.Code joinableValueType) {
304         switch (joinableValueType) {
305             case NONE:
306                 return AppSearchSchema.StringPropertyConfig.JOINABLE_VALUE_TYPE_NONE;
307             case QUALIFIED_ID:
308                 return AppSearchSchema.StringPropertyConfig.JOINABLE_VALUE_TYPE_QUALIFIED_ID;
309         }
310         // Avoid crashing in the 'read' path; we should try to interpret the document to the
311         // extent possible.
312         Log.w(TAG, "Invalid joinableValueType: " + joinableValueType.getNumber());
313         return AppSearchSchema.StringPropertyConfig.JOINABLE_VALUE_TYPE_NONE;
314     }
315 
316     @NonNull
convertTermMatchTypeToProto( @ppSearchSchema.StringPropertyConfig.IndexingType int indexingType)317     private static TermMatchType.Code convertTermMatchTypeToProto(
318             @AppSearchSchema.StringPropertyConfig.IndexingType int indexingType) {
319         switch (indexingType) {
320             case AppSearchSchema.StringPropertyConfig.INDEXING_TYPE_NONE:
321                 return TermMatchType.Code.UNKNOWN;
322             case AppSearchSchema.StringPropertyConfig.INDEXING_TYPE_EXACT_TERMS:
323                 return TermMatchType.Code.EXACT_ONLY;
324             case AppSearchSchema.StringPropertyConfig.INDEXING_TYPE_PREFIXES:
325                 return TermMatchType.Code.PREFIX;
326             default:
327                 throw new IllegalArgumentException("Invalid indexingType: " + indexingType);
328         }
329     }
330 
331     @AppSearchSchema.StringPropertyConfig.IndexingType
convertTermMatchTypeFromProto(@onNull TermMatchType.Code termMatchType)332     private static int convertTermMatchTypeFromProto(@NonNull TermMatchType.Code termMatchType) {
333         switch (termMatchType) {
334             case UNKNOWN:
335                 return AppSearchSchema.StringPropertyConfig.INDEXING_TYPE_NONE;
336             case EXACT_ONLY:
337                 return AppSearchSchema.StringPropertyConfig.INDEXING_TYPE_EXACT_TERMS;
338             case PREFIX:
339                 return AppSearchSchema.StringPropertyConfig.INDEXING_TYPE_PREFIXES;
340         }
341         // Avoid crashing in the 'read' path; we should try to interpret the document to the
342         // extent possible.
343         Log.w(TAG, "Invalid indexingType: " + termMatchType.getNumber());
344         return AppSearchSchema.StringPropertyConfig.INDEXING_TYPE_NONE;
345     }
346 
347     @NonNull
convertTokenizerTypeToProto( @ppSearchSchema.StringPropertyConfig.TokenizerType int tokenizerType)348     private static StringIndexingConfig.TokenizerType.Code convertTokenizerTypeToProto(
349             @AppSearchSchema.StringPropertyConfig.TokenizerType int tokenizerType) {
350         StringIndexingConfig.TokenizerType.Code tokenizerTypeProto =
351                 StringIndexingConfig.TokenizerType.Code.forNumber(tokenizerType);
352         if (tokenizerTypeProto == null) {
353             throw new IllegalArgumentException("Invalid tokenizerType: " + tokenizerType);
354         }
355         return tokenizerTypeProto;
356     }
357 
358     @NonNull
convertNumericMatchTypeToProto( @ppSearchSchema.LongPropertyConfig.IndexingType int indexingType)359     private static IntegerIndexingConfig.NumericMatchType.Code convertNumericMatchTypeToProto(
360             @AppSearchSchema.LongPropertyConfig.IndexingType int indexingType) {
361         switch (indexingType) {
362             case AppSearchSchema.LongPropertyConfig.INDEXING_TYPE_NONE:
363                 return IntegerIndexingConfig.NumericMatchType.Code.UNKNOWN;
364             case AppSearchSchema.LongPropertyConfig.INDEXING_TYPE_RANGE:
365                 return IntegerIndexingConfig.NumericMatchType.Code.RANGE;
366             default:
367                 throw new IllegalArgumentException("Invalid indexingType: " + indexingType);
368         }
369     }
370 
371     @AppSearchSchema.LongPropertyConfig.IndexingType
convertNumericMatchTypeFromProto( @onNull IntegerIndexingConfig.NumericMatchType.Code numericMatchType)372     private static int convertNumericMatchTypeFromProto(
373             @NonNull IntegerIndexingConfig.NumericMatchType.Code numericMatchType) {
374         switch (numericMatchType) {
375             case UNKNOWN:
376                 return AppSearchSchema.LongPropertyConfig.INDEXING_TYPE_NONE;
377             case RANGE:
378                 return AppSearchSchema.LongPropertyConfig.INDEXING_TYPE_RANGE;
379         }
380         // Avoid crashing in the 'read' path; we should try to interpret the document to the
381         // extent possible.
382         Log.w(TAG, "Invalid indexingType: " + numericMatchType.getNumber());
383         return AppSearchSchema.LongPropertyConfig.INDEXING_TYPE_NONE;
384     }
385 
386     @NonNull
387     private static EmbeddingIndexingConfig.EmbeddingIndexingType.Code
convertEmbeddingIndexingTypeToProto( @ppSearchSchema.EmbeddingPropertyConfig.IndexingType int indexingType)388             convertEmbeddingIndexingTypeToProto(
389                     @AppSearchSchema.EmbeddingPropertyConfig.IndexingType int indexingType) {
390         switch (indexingType) {
391             case AppSearchSchema.EmbeddingPropertyConfig.INDEXING_TYPE_NONE:
392                 return EmbeddingIndexingConfig.EmbeddingIndexingType.Code.UNKNOWN;
393             case AppSearchSchema.EmbeddingPropertyConfig.INDEXING_TYPE_SIMILARITY:
394                 return EmbeddingIndexingConfig.EmbeddingIndexingType.Code.LINEAR_SEARCH;
395             default:
396                 throw new IllegalArgumentException("Invalid indexingType: " + indexingType);
397         }
398     }
399 
400     @AppSearchSchema.EmbeddingPropertyConfig.IndexingType
convertEmbeddingIndexingTypeFromProto( @onNull EmbeddingIndexingConfig.EmbeddingIndexingType.Code indexingType)401     private static int convertEmbeddingIndexingTypeFromProto(
402             @NonNull EmbeddingIndexingConfig.EmbeddingIndexingType.Code indexingType) {
403         switch (indexingType) {
404             case UNKNOWN:
405                 return AppSearchSchema.EmbeddingPropertyConfig.INDEXING_TYPE_NONE;
406             case LINEAR_SEARCH:
407                 return AppSearchSchema.EmbeddingPropertyConfig.INDEXING_TYPE_SIMILARITY;
408         }
409         // Avoid crashing in the 'read' path; we should try to interpret the document to the
410         // extent possible.
411         Log.w(TAG, "Invalid indexingType: " + indexingType.getNumber());
412         return AppSearchSchema.EmbeddingPropertyConfig.INDEXING_TYPE_NONE;
413     }
414 }
415