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