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