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