1 /* 2 * Copyright (C) 2022 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.adservices.service.common.cache; 18 19 20 import androidx.annotation.NonNull; 21 import androidx.annotation.Nullable; 22 import androidx.room.ColumnInfo; 23 import androidx.room.Entity; 24 import androidx.room.Index; 25 import androidx.room.PrimaryKey; 26 import androidx.room.TypeConverter; 27 import androidx.room.TypeConverters; 28 29 import com.android.adservices.LogUtil; 30 31 import com.google.auto.value.AutoValue; 32 import com.google.common.collect.ImmutableMap; 33 34 import java.time.Instant; 35 import java.util.ArrayList; 36 import java.util.List; 37 import java.util.Map; 38 39 /** An entry that can be cached, for this class it contains a url and its response body */ 40 @AutoValue 41 @AutoValue.CopyAnnotations 42 @Entity( 43 tableName = "http_cache", 44 indices = {@Index(value = {"cache_url"})}) 45 @TypeConverters({DBCacheEntry.Converters.class}) 46 public abstract class DBCacheEntry { 47 48 /** @return Provides the URL which is the primary key for cached entry */ 49 @AutoValue.CopyAnnotations 50 @ColumnInfo(name = "cache_url") 51 @NonNull 52 @PrimaryKey getUrl()53 public abstract String getUrl(); 54 55 /** @return the response body corresponding to the cached url */ 56 @AutoValue.CopyAnnotations 57 @ColumnInfo(name = "response_body") getResponseBody()58 public abstract String getResponseBody(); 59 60 /** @return the response headers corresponding to the cached url */ 61 @AutoValue.CopyAnnotations 62 @ColumnInfo(name = "response_headers") getResponseHeaders()63 public abstract ImmutableMap<String, List<String>> getResponseHeaders(); 64 65 /** @return the timestamp at which this entry was cached */ 66 @AutoValue.CopyAnnotations 67 @ColumnInfo(name = "creation_timestamp") getCreationTimestamp()68 public abstract Instant getCreationTimestamp(); 69 70 /** @return max time in second for which this entry should be considered fresh */ 71 @AutoValue.CopyAnnotations 72 @ColumnInfo(name = "max_age") getMaxAgeSeconds()73 public abstract long getMaxAgeSeconds(); 74 75 /** 76 * Creates an entry that can be persisted in the cache storage 77 * 78 * @param url for which the request needs to be cached 79 * @param responseBody response for the url request made 80 * @param responseHeaders headers for the response corresponding to the request made by url 81 * @param creationTimestamp time at which the request is persisted 82 * @param maxAgeSeconds time for which this cache entry is considered fresh 83 * @return an instance or created {@link DBCacheEntry} 84 */ create( @onNull String url, String responseBody, ImmutableMap<String, List<String>> responseHeaders, Instant creationTimestamp, long maxAgeSeconds)85 public static DBCacheEntry create( 86 @NonNull String url, 87 String responseBody, 88 ImmutableMap<String, List<String>> responseHeaders, 89 Instant creationTimestamp, 90 long maxAgeSeconds) { 91 return builder() 92 .setUrl(url) 93 .setResponseBody(responseBody) 94 .setResponseHeaders(responseHeaders) 95 .setCreationTimestamp(creationTimestamp) 96 .setMaxAgeSeconds(maxAgeSeconds) 97 .build(); 98 } 99 100 /** @return a builder to construct an instance of {@link DBCacheEntry} */ builder()101 public static DBCacheEntry.Builder builder() { 102 return new AutoValue_DBCacheEntry.Builder().setResponseHeaders(ImmutableMap.of()); 103 } 104 105 /** Provides a builder for creating a {@link DBCacheEntry} */ 106 @AutoValue.Builder 107 public abstract static class Builder { 108 109 /** Sets the Url for which the entry is cached */ setUrl(String url)110 public abstract DBCacheEntry.Builder setUrl(String url); 111 112 /** sets the response body corresponding to the URL */ setResponseBody(String responseBody)113 public abstract DBCacheEntry.Builder setResponseBody(String responseBody); 114 115 /** sets the response headers corresponding to the URL */ setResponseHeaders( ImmutableMap<String, List<String>> responseHeaders)116 public abstract DBCacheEntry.Builder setResponseHeaders( 117 ImmutableMap<String, List<String>> responseHeaders); 118 119 /** Sets the creation timestamp of the cached entry */ setCreationTimestamp(Instant creationTimestamp)120 public abstract DBCacheEntry.Builder setCreationTimestamp(Instant creationTimestamp); 121 122 /** Sets the maxAge in seconds for which the entry is considered fresh */ setMaxAgeSeconds(long maxAgeSeconds)123 public abstract DBCacheEntry.Builder setMaxAgeSeconds(long maxAgeSeconds); 124 125 /** 126 * Returns a {@link com.android.adservices.service.common.cache.DBCacheEntry} build with the 127 * information provided in this builder * 128 */ build()129 public abstract DBCacheEntry build(); 130 } 131 132 /** 133 * Converters to help serialize and deserialize objects from them to be persisted and retrieved 134 * from DB. 135 */ 136 public static class Converters { 137 private static final String KEY_VALUE_SEPARATOR = "="; 138 private static final String VALUES_SEPARATOR = ","; 139 private static final String ENTRIES_SEPARATOR = ";"; 140 Converters()141 private Converters() {} 142 143 /** 144 * @param responseHeaders a map of response headers for a web request 145 * @return serialized version of response headers 146 */ 147 @TypeConverter serializeResponseHeaders( @ullable ImmutableMap<String, List<String>> responseHeaders)148 public static String serializeResponseHeaders( 149 @Nullable ImmutableMap<String, List<String>> responseHeaders) { 150 if (responseHeaders == null || responseHeaders.isEmpty()) { 151 return ""; 152 } 153 try { 154 List<String> serializedHeaders = new ArrayList<>(); 155 StringBuilder sb = new StringBuilder(); 156 for (Map.Entry<String, List<String>> entry : responseHeaders.entrySet()) { 157 if (entry.getKey() != null && !entry.getValue().isEmpty()) { 158 sb.append(entry.getKey() + KEY_VALUE_SEPARATOR); 159 sb.append(String.join(VALUES_SEPARATOR, entry.getValue()).trim()); 160 serializedHeaders.add(sb.toString()); 161 sb.setLength(0); 162 } 163 } 164 return String.join(ENTRIES_SEPARATOR, serializedHeaders); 165 } catch (Exception e) { 166 LogUtil.e(e, "Failed to serialize response headers"); 167 } 168 return ""; 169 } 170 171 /** 172 * @param responseHeadersString serialized version of response headers 173 * @return a map of response headers for a web request 174 */ 175 @TypeConverter deserializeResponseHeaders( @ullable String responseHeadersString)176 public static ImmutableMap<String, List<String>> deserializeResponseHeaders( 177 @Nullable String responseHeadersString) { 178 ImmutableMap.Builder<String, List<String>> responseHeadersBuilder = 179 ImmutableMap.builder(); 180 if (responseHeadersString == null || responseHeadersString.isEmpty()) { 181 return responseHeadersBuilder.build(); 182 } 183 try { 184 String[] deserializedHeaders = responseHeadersString.split(ENTRIES_SEPARATOR); 185 for (String entry : deserializedHeaders) { 186 String[] keyValuePair = entry.split(KEY_VALUE_SEPARATOR); 187 if (!keyValuePair[0].isEmpty() && !keyValuePair[1].isEmpty()) { 188 List<String> list = new ArrayList<>(); 189 for (String x : keyValuePair[1].split(VALUES_SEPARATOR)) { 190 String s = x.trim(); 191 if (!s.isEmpty()) { 192 list.add(s); 193 } 194 } 195 responseHeadersBuilder.put(keyValuePair[0], list); 196 } 197 } 198 } catch (Exception e) { 199 LogUtil.e(e, "Failed to deserialize response headers"); 200 } 201 return responseHeadersBuilder.build(); 202 } 203 } 204 } 205