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.app.appsearch.GenericDocument;
22 
23 import com.google.android.icing.proto.DocumentProto;
24 import com.google.android.icing.proto.PropertyProto;
25 import com.google.android.icing.proto.SchemaTypeConfigProto;
26 import com.google.protobuf.ByteString;
27 
28 import java.util.ArrayList;
29 import java.util.Collections;
30 import java.util.Map;
31 import java.util.Objects;
32 
33 /**
34  * Translates a {@link GenericDocument} into a {@link DocumentProto}.
35  *
36  * @hide
37  */
38 public final class GenericDocumentToProtoConverter {
39     private static final String[] EMPTY_STRING_ARRAY = new String[0];
40     private static final long[] EMPTY_LONG_ARRAY = new long[0];
41     private static final double[] EMPTY_DOUBLE_ARRAY = new double[0];
42     private static final boolean[] EMPTY_BOOLEAN_ARRAY = new boolean[0];
43     private static final byte[][] EMPTY_BYTES_ARRAY = new byte[0][0];
44     private static final GenericDocument[] EMPTY_DOCUMENT_ARRAY = new GenericDocument[0];
45 
GenericDocumentToProtoConverter()46     private GenericDocumentToProtoConverter() {}
47 
48     /** Converts a {@link GenericDocument} into a {@link DocumentProto}. */
49     @NonNull
50     @SuppressWarnings("unchecked")
toDocumentProto(@onNull GenericDocument document)51     public static DocumentProto toDocumentProto(@NonNull GenericDocument document) {
52         Objects.requireNonNull(document);
53         DocumentProto.Builder mProtoBuilder = DocumentProto.newBuilder();
54         mProtoBuilder
55                 .setUri(document.getId())
56                 .setSchema(document.getSchemaType())
57                 .setNamespace(document.getNamespace())
58                 .setScore(document.getScore())
59                 .setTtlMs(document.getTtlMillis())
60                 .setCreationTimestampMs(document.getCreationTimestampMillis());
61         ArrayList<String> keys = new ArrayList<>(document.getPropertyNames());
62         Collections.sort(keys);
63         for (int i = 0; i < keys.size(); i++) {
64             String name = keys.get(i);
65             PropertyProto.Builder propertyProto = PropertyProto.newBuilder().setName(name);
66             Object property = document.getProperty(name);
67             if (property instanceof String[]) {
68                 String[] stringValues = (String[]) property;
69                 for (int j = 0; j < stringValues.length; j++) {
70                     propertyProto.addStringValues(stringValues[j]);
71                 }
72             } else if (property instanceof long[]) {
73                 long[] longValues = (long[]) property;
74                 for (int j = 0; j < longValues.length; j++) {
75                     propertyProto.addInt64Values(longValues[j]);
76                 }
77             } else if (property instanceof double[]) {
78                 double[] doubleValues = (double[]) property;
79                 for (int j = 0; j < doubleValues.length; j++) {
80                     propertyProto.addDoubleValues(doubleValues[j]);
81                 }
82             } else if (property instanceof boolean[]) {
83                 boolean[] booleanValues = (boolean[]) property;
84                 for (int j = 0; j < booleanValues.length; j++) {
85                     propertyProto.addBooleanValues(booleanValues[j]);
86                 }
87             } else if (property instanceof byte[][]) {
88                 byte[][] bytesValues = (byte[][]) property;
89                 for (int j = 0; j < bytesValues.length; j++) {
90                     propertyProto.addBytesValues(ByteString.copyFrom(bytesValues[j]));
91                 }
92             } else if (property instanceof GenericDocument[]) {
93                 GenericDocument[] documentValues = (GenericDocument[]) property;
94                 for (int j = 0; j < documentValues.length; j++) {
95                     DocumentProto proto = toDocumentProto(documentValues[j]);
96                     propertyProto.addDocumentValues(proto);
97                 }
98             } else {
99                 throw new IllegalStateException(
100                         String.format(
101                                 "Property \"%s\" has unsupported value type %s",
102                                 name, property.getClass().toString()));
103             }
104             mProtoBuilder.addProperties(propertyProto);
105         }
106         return mProtoBuilder.build();
107     }
108 
109     /**
110      * Converts a {@link DocumentProto} into a {@link GenericDocument}.
111      *
112      * <p>In the case that the {@link DocumentProto} object proto has no values set, the converter
113      * searches for the matching property name in the {@link SchemaTypeConfigProto} object for the
114      * document, and infers the correct default value to set for the empty property based on the
115      * data type of the property defined by the schema type.
116      *
117      * @param proto the document to convert to a {@link GenericDocument} instance. The document
118      *     proto should have its package + database prefix stripped from its fields.
119      * @param prefix the package + database prefix used searching the {@code schemaTypeMap}.
120      * @param schemaTypeMap map of prefixed schema type to {@link SchemaTypeConfigProto}, used for
121      *     looking up the default empty value to set for a document property that has all empty
122      *     values.
123      */
124     @NonNull
toGenericDocument( @onNull DocumentProto proto, @NonNull String prefix, @NonNull Map<String, SchemaTypeConfigProto> schemaTypeMap)125     public static GenericDocument toGenericDocument(
126             @NonNull DocumentProto proto,
127             @NonNull String prefix,
128             @NonNull Map<String, SchemaTypeConfigProto> schemaTypeMap) {
129         Objects.requireNonNull(proto);
130         GenericDocument.Builder<?> documentBuilder =
131                 new GenericDocument.Builder<>(
132                                 proto.getNamespace(), proto.getUri(), proto.getSchema())
133                         .setScore(proto.getScore())
134                         .setTtlMillis(proto.getTtlMs())
135                         .setCreationTimestampMillis(proto.getCreationTimestampMs());
136         String prefixedSchemaType = prefix + proto.getSchema();
137 
138         for (int i = 0; i < proto.getPropertiesCount(); i++) {
139             PropertyProto property = proto.getProperties(i);
140             String name = property.getName();
141             if (property.getStringValuesCount() > 0) {
142                 String[] values = new String[property.getStringValuesCount()];
143                 for (int j = 0; j < values.length; j++) {
144                     values[j] = property.getStringValues(j);
145                 }
146                 documentBuilder.setPropertyString(name, values);
147             } else if (property.getInt64ValuesCount() > 0) {
148                 long[] values = new long[property.getInt64ValuesCount()];
149                 for (int j = 0; j < values.length; j++) {
150                     values[j] = property.getInt64Values(j);
151                 }
152                 documentBuilder.setPropertyLong(name, values);
153             } else if (property.getDoubleValuesCount() > 0) {
154                 double[] values = new double[property.getDoubleValuesCount()];
155                 for (int j = 0; j < values.length; j++) {
156                     values[j] = property.getDoubleValues(j);
157                 }
158                 documentBuilder.setPropertyDouble(name, values);
159             } else if (property.getBooleanValuesCount() > 0) {
160                 boolean[] values = new boolean[property.getBooleanValuesCount()];
161                 for (int j = 0; j < values.length; j++) {
162                     values[j] = property.getBooleanValues(j);
163                 }
164                 documentBuilder.setPropertyBoolean(name, values);
165             } else if (property.getBytesValuesCount() > 0) {
166                 byte[][] values = new byte[property.getBytesValuesCount()][];
167                 for (int j = 0; j < values.length; j++) {
168                     values[j] = property.getBytesValues(j).toByteArray();
169                 }
170                 documentBuilder.setPropertyBytes(name, values);
171             } else if (property.getDocumentValuesCount() > 0) {
172                 GenericDocument[] values = new GenericDocument[property.getDocumentValuesCount()];
173                 for (int j = 0; j < values.length; j++) {
174                     values[j] =
175                             toGenericDocument(property.getDocumentValues(j), prefix, schemaTypeMap);
176                 }
177                 documentBuilder.setPropertyDocument(name, values);
178             } else {
179                 // TODO(b/184966497): Optimize by caching PropertyConfigProto
180                 setEmptyProperty(name, documentBuilder, schemaTypeMap.get(prefixedSchemaType));
181             }
182         }
183         return documentBuilder.build();
184     }
185 
setEmptyProperty( @onNull String propertyName, @NonNull GenericDocument.Builder<?> documentBuilder, @NonNull SchemaTypeConfigProto schema)186     private static void setEmptyProperty(
187             @NonNull String propertyName,
188             @NonNull GenericDocument.Builder<?> documentBuilder,
189             @NonNull SchemaTypeConfigProto schema) {
190         @AppSearchSchema.PropertyConfig.DataType int dataType = 0;
191         for (int i = 0; i < schema.getPropertiesCount(); ++i) {
192             if (propertyName.equals(schema.getProperties(i).getPropertyName())) {
193                 dataType = schema.getProperties(i).getDataType().getNumber();
194                 break;
195             }
196         }
197 
198         switch (dataType) {
199             case AppSearchSchema.PropertyConfig.DATA_TYPE_STRING:
200                 documentBuilder.setPropertyString(propertyName, EMPTY_STRING_ARRAY);
201                 break;
202             case AppSearchSchema.PropertyConfig.DATA_TYPE_LONG:
203                 documentBuilder.setPropertyLong(propertyName, EMPTY_LONG_ARRAY);
204                 break;
205             case AppSearchSchema.PropertyConfig.DATA_TYPE_DOUBLE:
206                 documentBuilder.setPropertyDouble(propertyName, EMPTY_DOUBLE_ARRAY);
207                 break;
208             case AppSearchSchema.PropertyConfig.DATA_TYPE_BOOLEAN:
209                 documentBuilder.setPropertyBoolean(propertyName, EMPTY_BOOLEAN_ARRAY);
210                 break;
211             case AppSearchSchema.PropertyConfig.DATA_TYPE_BYTES:
212                 documentBuilder.setPropertyBytes(propertyName, EMPTY_BYTES_ARRAY);
213                 break;
214             case AppSearchSchema.PropertyConfig.DATA_TYPE_DOCUMENT:
215                 documentBuilder.setPropertyDocument(propertyName, EMPTY_DOCUMENT_ARRAY);
216                 break;
217             default:
218                 throw new IllegalStateException("Unknown type of value: " + propertyName);
219         }
220     }
221 }
222