16a34bb2d6a6cbc7a70bdf0c53d238dc28e0b1d58Joseph Wen/* 26a34bb2d6a6cbc7a70bdf0c53d238dc28e0b1d58Joseph Wen * Copyright (C) 2015 The Android Open Source Project 36a34bb2d6a6cbc7a70bdf0c53d238dc28e0b1d58Joseph Wen * 46a34bb2d6a6cbc7a70bdf0c53d238dc28e0b1d58Joseph Wen * Licensed under the Apache License, Version 2.0 (the "License"); 56a34bb2d6a6cbc7a70bdf0c53d238dc28e0b1d58Joseph Wen * you may not use this file except in compliance with the License. 66a34bb2d6a6cbc7a70bdf0c53d238dc28e0b1d58Joseph Wen * You may obtain a copy of the License at 76a34bb2d6a6cbc7a70bdf0c53d238dc28e0b1d58Joseph Wen * 86a34bb2d6a6cbc7a70bdf0c53d238dc28e0b1d58Joseph Wen * http://www.apache.org/licenses/LICENSE-2.0 96a34bb2d6a6cbc7a70bdf0c53d238dc28e0b1d58Joseph Wen * 106a34bb2d6a6cbc7a70bdf0c53d238dc28e0b1d58Joseph Wen * Unless required by applicable law or agreed to in writing, software 116a34bb2d6a6cbc7a70bdf0c53d238dc28e0b1d58Joseph Wen * distributed under the License is distributed on an "AS IS" BASIS, 126a34bb2d6a6cbc7a70bdf0c53d238dc28e0b1d58Joseph Wen * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 136a34bb2d6a6cbc7a70bdf0c53d238dc28e0b1d58Joseph Wen * See the License for the specific language governing permissions and 146a34bb2d6a6cbc7a70bdf0c53d238dc28e0b1d58Joseph Wen * limitations under the License. 156a34bb2d6a6cbc7a70bdf0c53d238dc28e0b1d58Joseph Wen */ 166a34bb2d6a6cbc7a70bdf0c53d238dc28e0b1d58Joseph Wen 176a34bb2d6a6cbc7a70bdf0c53d238dc28e0b1d58Joseph Wenpackage com.android.statementservice.retriever; 186a34bb2d6a6cbc7a70bdf0c53d238dc28e0b1d58Joseph Wen 198c7d99c2b77acbcbbdcbf0dcab61a07767d5dd1bJoseph Wenimport android.util.Log; 208c7d99c2b77acbcbbdcbf0dcab61a07767d5dd1bJoseph Wen 216a34bb2d6a6cbc7a70bdf0c53d238dc28e0b1d58Joseph Wenimport com.android.volley.Cache; 226a34bb2d6a6cbc7a70bdf0c53d238dc28e0b1d58Joseph Wenimport com.android.volley.NetworkResponse; 236a34bb2d6a6cbc7a70bdf0c53d238dc28e0b1d58Joseph Wenimport com.android.volley.toolbox.HttpHeaderParser; 246a34bb2d6a6cbc7a70bdf0c53d238dc28e0b1d58Joseph Wen 256a34bb2d6a6cbc7a70bdf0c53d238dc28e0b1d58Joseph Wenimport java.io.BufferedInputStream; 266a34bb2d6a6cbc7a70bdf0c53d238dc28e0b1d58Joseph Wenimport java.io.ByteArrayOutputStream; 276a34bb2d6a6cbc7a70bdf0c53d238dc28e0b1d58Joseph Wenimport java.io.IOException; 286a34bb2d6a6cbc7a70bdf0c53d238dc28e0b1d58Joseph Wenimport java.io.InputStream; 296a34bb2d6a6cbc7a70bdf0c53d238dc28e0b1d58Joseph Wenimport java.net.HttpURLConnection; 306a34bb2d6a6cbc7a70bdf0c53d238dc28e0b1d58Joseph Wenimport java.net.URL; 316a34bb2d6a6cbc7a70bdf0c53d238dc28e0b1d58Joseph Wenimport java.util.HashMap; 326a34bb2d6a6cbc7a70bdf0c53d238dc28e0b1d58Joseph Wenimport java.util.List; 336a34bb2d6a6cbc7a70bdf0c53d238dc28e0b1d58Joseph Wenimport java.util.Locale; 346a34bb2d6a6cbc7a70bdf0c53d238dc28e0b1d58Joseph Wenimport java.util.Map; 356a34bb2d6a6cbc7a70bdf0c53d238dc28e0b1d58Joseph Wen 366a34bb2d6a6cbc7a70bdf0c53d238dc28e0b1d58Joseph Wen/** 376a34bb2d6a6cbc7a70bdf0c53d238dc28e0b1d58Joseph Wen * Helper class for fetching HTTP or HTTPS URL. 386a34bb2d6a6cbc7a70bdf0c53d238dc28e0b1d58Joseph Wen * 396a34bb2d6a6cbc7a70bdf0c53d238dc28e0b1d58Joseph Wen * Visible for testing. 406a34bb2d6a6cbc7a70bdf0c53d238dc28e0b1d58Joseph Wen * 416a34bb2d6a6cbc7a70bdf0c53d238dc28e0b1d58Joseph Wen * @hide 426a34bb2d6a6cbc7a70bdf0c53d238dc28e0b1d58Joseph Wen */ 436a34bb2d6a6cbc7a70bdf0c53d238dc28e0b1d58Joseph Wenpublic class URLFetcher { 448c7d99c2b77acbcbbdcbf0dcab61a07767d5dd1bJoseph Wen private static final String TAG = URLFetcher.class.getSimpleName(); 456a34bb2d6a6cbc7a70bdf0c53d238dc28e0b1d58Joseph Wen 466a34bb2d6a6cbc7a70bdf0c53d238dc28e0b1d58Joseph Wen private static final long DO_NOT_CACHE_RESULT = 0L; 476a34bb2d6a6cbc7a70bdf0c53d238dc28e0b1d58Joseph Wen private static final int INPUT_BUFFER_SIZE_IN_BYTES = 1024; 486a34bb2d6a6cbc7a70bdf0c53d238dc28e0b1d58Joseph Wen 496a34bb2d6a6cbc7a70bdf0c53d238dc28e0b1d58Joseph Wen /** 506a34bb2d6a6cbc7a70bdf0c53d238dc28e0b1d58Joseph Wen * Fetches the specified url and returns the content and ttl. 516a34bb2d6a6cbc7a70bdf0c53d238dc28e0b1d58Joseph Wen * 525fcbb9d0c17e073fb28c7169e80fa7bb8e61c6b1Joseph Wen * <p> 535fcbb9d0c17e073fb28c7169e80fa7bb8e61c6b1Joseph Wen * Retry {@code retry} times if the connection failed or timed out for any reason. 545fcbb9d0c17e073fb28c7169e80fa7bb8e61c6b1Joseph Wen * HTTP error code (e.g. 404/500) won't be retried. 555fcbb9d0c17e073fb28c7169e80fa7bb8e61c6b1Joseph Wen * 565fcbb9d0c17e073fb28c7169e80fa7bb8e61c6b1Joseph Wen * @throws IOException if it can't retrieve the content due to a network problem. 575fcbb9d0c17e073fb28c7169e80fa7bb8e61c6b1Joseph Wen * @throws AssociationServiceException if the URL scheme is not http or https or the content 585fcbb9d0c17e073fb28c7169e80fa7bb8e61c6b1Joseph Wen * length exceeds {code fileSizeLimit}. 595fcbb9d0c17e073fb28c7169e80fa7bb8e61c6b1Joseph Wen */ 605fcbb9d0c17e073fb28c7169e80fa7bb8e61c6b1Joseph Wen public WebContent getWebContentFromUrlWithRetry(URL url, long fileSizeLimit, 615fcbb9d0c17e073fb28c7169e80fa7bb8e61c6b1Joseph Wen int connectionTimeoutMillis, int backoffMillis, int retry) 625fcbb9d0c17e073fb28c7169e80fa7bb8e61c6b1Joseph Wen throws AssociationServiceException, IOException, InterruptedException { 635fcbb9d0c17e073fb28c7169e80fa7bb8e61c6b1Joseph Wen if (retry <= 0) { 645fcbb9d0c17e073fb28c7169e80fa7bb8e61c6b1Joseph Wen throw new IllegalArgumentException("retry should be a postive inetger."); 655fcbb9d0c17e073fb28c7169e80fa7bb8e61c6b1Joseph Wen } 665fcbb9d0c17e073fb28c7169e80fa7bb8e61c6b1Joseph Wen while (retry > 0) { 675fcbb9d0c17e073fb28c7169e80fa7bb8e61c6b1Joseph Wen try { 685fcbb9d0c17e073fb28c7169e80fa7bb8e61c6b1Joseph Wen return getWebContentFromUrl(url, fileSizeLimit, connectionTimeoutMillis); 695fcbb9d0c17e073fb28c7169e80fa7bb8e61c6b1Joseph Wen } catch (IOException e) { 705fcbb9d0c17e073fb28c7169e80fa7bb8e61c6b1Joseph Wen retry--; 715fcbb9d0c17e073fb28c7169e80fa7bb8e61c6b1Joseph Wen if (retry == 0) { 725fcbb9d0c17e073fb28c7169e80fa7bb8e61c6b1Joseph Wen throw e; 735fcbb9d0c17e073fb28c7169e80fa7bb8e61c6b1Joseph Wen } 745fcbb9d0c17e073fb28c7169e80fa7bb8e61c6b1Joseph Wen } 755fcbb9d0c17e073fb28c7169e80fa7bb8e61c6b1Joseph Wen 765fcbb9d0c17e073fb28c7169e80fa7bb8e61c6b1Joseph Wen Thread.sleep(backoffMillis); 775fcbb9d0c17e073fb28c7169e80fa7bb8e61c6b1Joseph Wen } 785fcbb9d0c17e073fb28c7169e80fa7bb8e61c6b1Joseph Wen 795fcbb9d0c17e073fb28c7169e80fa7bb8e61c6b1Joseph Wen // Should never reach here. 805fcbb9d0c17e073fb28c7169e80fa7bb8e61c6b1Joseph Wen return null; 815fcbb9d0c17e073fb28c7169e80fa7bb8e61c6b1Joseph Wen } 825fcbb9d0c17e073fb28c7169e80fa7bb8e61c6b1Joseph Wen 835fcbb9d0c17e073fb28c7169e80fa7bb8e61c6b1Joseph Wen /** 845fcbb9d0c17e073fb28c7169e80fa7bb8e61c6b1Joseph Wen * Fetches the specified url and returns the content and ttl. 855fcbb9d0c17e073fb28c7169e80fa7bb8e61c6b1Joseph Wen * 866a34bb2d6a6cbc7a70bdf0c53d238dc28e0b1d58Joseph Wen * @throws IOException if it can't retrieve the content due to a network problem. 876a34bb2d6a6cbc7a70bdf0c53d238dc28e0b1d58Joseph Wen * @throws AssociationServiceException if the URL scheme is not http or https or the content 886a34bb2d6a6cbc7a70bdf0c53d238dc28e0b1d58Joseph Wen * length exceeds {code fileSizeLimit}. 896a34bb2d6a6cbc7a70bdf0c53d238dc28e0b1d58Joseph Wen */ 906a34bb2d6a6cbc7a70bdf0c53d238dc28e0b1d58Joseph Wen public WebContent getWebContentFromUrl(URL url, long fileSizeLimit, int connectionTimeoutMillis) 916a34bb2d6a6cbc7a70bdf0c53d238dc28e0b1d58Joseph Wen throws AssociationServiceException, IOException { 926a34bb2d6a6cbc7a70bdf0c53d238dc28e0b1d58Joseph Wen final String scheme = url.getProtocol().toLowerCase(Locale.US); 936a34bb2d6a6cbc7a70bdf0c53d238dc28e0b1d58Joseph Wen if (!scheme.equals("http") && !scheme.equals("https")) { 946a34bb2d6a6cbc7a70bdf0c53d238dc28e0b1d58Joseph Wen throw new IllegalArgumentException("The url protocol should be on http or https."); 956a34bb2d6a6cbc7a70bdf0c53d238dc28e0b1d58Joseph Wen } 966a34bb2d6a6cbc7a70bdf0c53d238dc28e0b1d58Joseph Wen 97871fe6ed66e9de1369fbc7e4a145f98272b88c0bJoseph Wen HttpURLConnection connection = null; 98871fe6ed66e9de1369fbc7e4a145f98272b88c0bJoseph Wen try { 99871fe6ed66e9de1369fbc7e4a145f98272b88c0bJoseph Wen connection = (HttpURLConnection) url.openConnection(); 100871fe6ed66e9de1369fbc7e4a145f98272b88c0bJoseph Wen connection.setInstanceFollowRedirects(true); 101871fe6ed66e9de1369fbc7e4a145f98272b88c0bJoseph Wen connection.setConnectTimeout(connectionTimeoutMillis); 102871fe6ed66e9de1369fbc7e4a145f98272b88c0bJoseph Wen connection.setReadTimeout(connectionTimeoutMillis); 103871fe6ed66e9de1369fbc7e4a145f98272b88c0bJoseph Wen connection.setUseCaches(true); 104871fe6ed66e9de1369fbc7e4a145f98272b88c0bJoseph Wen connection.setInstanceFollowRedirects(false); 105871fe6ed66e9de1369fbc7e4a145f98272b88c0bJoseph Wen connection.addRequestProperty("Cache-Control", "max-stale=60"); 1066a34bb2d6a6cbc7a70bdf0c53d238dc28e0b1d58Joseph Wen 107871fe6ed66e9de1369fbc7e4a145f98272b88c0bJoseph Wen if (connection.getResponseCode() != HttpURLConnection.HTTP_OK) { 108871fe6ed66e9de1369fbc7e4a145f98272b88c0bJoseph Wen Log.e(TAG, "The responses code is not 200 but " + connection.getResponseCode()); 109871fe6ed66e9de1369fbc7e4a145f98272b88c0bJoseph Wen return new WebContent("", DO_NOT_CACHE_RESULT); 110871fe6ed66e9de1369fbc7e4a145f98272b88c0bJoseph Wen } 1118c7d99c2b77acbcbbdcbf0dcab61a07767d5dd1bJoseph Wen 112871fe6ed66e9de1369fbc7e4a145f98272b88c0bJoseph Wen if (connection.getContentLength() > fileSizeLimit) { 113871fe6ed66e9de1369fbc7e4a145f98272b88c0bJoseph Wen Log.e(TAG, "The content size of the url is larger than " + fileSizeLimit); 114871fe6ed66e9de1369fbc7e4a145f98272b88c0bJoseph Wen return new WebContent("", DO_NOT_CACHE_RESULT); 115871fe6ed66e9de1369fbc7e4a145f98272b88c0bJoseph Wen } 1166a34bb2d6a6cbc7a70bdf0c53d238dc28e0b1d58Joseph Wen 117871fe6ed66e9de1369fbc7e4a145f98272b88c0bJoseph Wen Long expireTimeMillis = getExpirationTimeMillisFromHTTPHeader( 118871fe6ed66e9de1369fbc7e4a145f98272b88c0bJoseph Wen connection.getHeaderFields()); 1196a34bb2d6a6cbc7a70bdf0c53d238dc28e0b1d58Joseph Wen 1206a34bb2d6a6cbc7a70bdf0c53d238dc28e0b1d58Joseph Wen return new WebContent(inputStreamToString( 1216a34bb2d6a6cbc7a70bdf0c53d238dc28e0b1d58Joseph Wen connection.getInputStream(), connection.getContentLength(), fileSizeLimit), 1226a34bb2d6a6cbc7a70bdf0c53d238dc28e0b1d58Joseph Wen expireTimeMillis); 1236a34bb2d6a6cbc7a70bdf0c53d238dc28e0b1d58Joseph Wen } finally { 124871fe6ed66e9de1369fbc7e4a145f98272b88c0bJoseph Wen if (connection != null) { 125871fe6ed66e9de1369fbc7e4a145f98272b88c0bJoseph Wen connection.disconnect(); 126871fe6ed66e9de1369fbc7e4a145f98272b88c0bJoseph Wen } 1276a34bb2d6a6cbc7a70bdf0c53d238dc28e0b1d58Joseph Wen } 1286a34bb2d6a6cbc7a70bdf0c53d238dc28e0b1d58Joseph Wen } 1296a34bb2d6a6cbc7a70bdf0c53d238dc28e0b1d58Joseph Wen 1306a34bb2d6a6cbc7a70bdf0c53d238dc28e0b1d58Joseph Wen /** 1316a34bb2d6a6cbc7a70bdf0c53d238dc28e0b1d58Joseph Wen * Visible for testing. 1326a34bb2d6a6cbc7a70bdf0c53d238dc28e0b1d58Joseph Wen * @hide 1336a34bb2d6a6cbc7a70bdf0c53d238dc28e0b1d58Joseph Wen */ 1346a34bb2d6a6cbc7a70bdf0c53d238dc28e0b1d58Joseph Wen public static String inputStreamToString(InputStream inputStream, int length, long sizeLimit) 1356a34bb2d6a6cbc7a70bdf0c53d238dc28e0b1d58Joseph Wen throws IOException, AssociationServiceException { 1366a34bb2d6a6cbc7a70bdf0c53d238dc28e0b1d58Joseph Wen if (length < 0) { 1376a34bb2d6a6cbc7a70bdf0c53d238dc28e0b1d58Joseph Wen length = 0; 1386a34bb2d6a6cbc7a70bdf0c53d238dc28e0b1d58Joseph Wen } 1396a34bb2d6a6cbc7a70bdf0c53d238dc28e0b1d58Joseph Wen ByteArrayOutputStream baos = new ByteArrayOutputStream(length); 1406a34bb2d6a6cbc7a70bdf0c53d238dc28e0b1d58Joseph Wen BufferedInputStream bis = new BufferedInputStream(inputStream); 1416a34bb2d6a6cbc7a70bdf0c53d238dc28e0b1d58Joseph Wen byte[] buffer = new byte[INPUT_BUFFER_SIZE_IN_BYTES]; 1426a34bb2d6a6cbc7a70bdf0c53d238dc28e0b1d58Joseph Wen int len = 0; 1436a34bb2d6a6cbc7a70bdf0c53d238dc28e0b1d58Joseph Wen while ((len = bis.read(buffer)) != -1) { 1446a34bb2d6a6cbc7a70bdf0c53d238dc28e0b1d58Joseph Wen baos.write(buffer, 0, len); 1456a34bb2d6a6cbc7a70bdf0c53d238dc28e0b1d58Joseph Wen if (baos.size() > sizeLimit) { 1466a34bb2d6a6cbc7a70bdf0c53d238dc28e0b1d58Joseph Wen throw new AssociationServiceException("The content size of the url is larger than " 1476a34bb2d6a6cbc7a70bdf0c53d238dc28e0b1d58Joseph Wen + sizeLimit); 1486a34bb2d6a6cbc7a70bdf0c53d238dc28e0b1d58Joseph Wen } 1496a34bb2d6a6cbc7a70bdf0c53d238dc28e0b1d58Joseph Wen } 1506a34bb2d6a6cbc7a70bdf0c53d238dc28e0b1d58Joseph Wen return baos.toString("UTF-8"); 1516a34bb2d6a6cbc7a70bdf0c53d238dc28e0b1d58Joseph Wen } 1526a34bb2d6a6cbc7a70bdf0c53d238dc28e0b1d58Joseph Wen 1536a34bb2d6a6cbc7a70bdf0c53d238dc28e0b1d58Joseph Wen /** 1546a34bb2d6a6cbc7a70bdf0c53d238dc28e0b1d58Joseph Wen * Parses the HTTP headers to compute the ttl. 1556a34bb2d6a6cbc7a70bdf0c53d238dc28e0b1d58Joseph Wen * 1566a34bb2d6a6cbc7a70bdf0c53d238dc28e0b1d58Joseph Wen * @param headers a map that map the header key to the header values. Can be null. 1576a34bb2d6a6cbc7a70bdf0c53d238dc28e0b1d58Joseph Wen * @return the ttl in millisecond or null if the ttl is not specified in the header. 1586a34bb2d6a6cbc7a70bdf0c53d238dc28e0b1d58Joseph Wen */ 1596a34bb2d6a6cbc7a70bdf0c53d238dc28e0b1d58Joseph Wen private Long getExpirationTimeMillisFromHTTPHeader(Map<String, List<String>> headers) { 1606a34bb2d6a6cbc7a70bdf0c53d238dc28e0b1d58Joseph Wen if (headers == null) { 1616a34bb2d6a6cbc7a70bdf0c53d238dc28e0b1d58Joseph Wen return null; 1626a34bb2d6a6cbc7a70bdf0c53d238dc28e0b1d58Joseph Wen } 1636a34bb2d6a6cbc7a70bdf0c53d238dc28e0b1d58Joseph Wen Map<String, String> joinedHeaders = joinHttpHeaders(headers); 1646a34bb2d6a6cbc7a70bdf0c53d238dc28e0b1d58Joseph Wen 1656a34bb2d6a6cbc7a70bdf0c53d238dc28e0b1d58Joseph Wen NetworkResponse response = new NetworkResponse(null, joinedHeaders); 1666a34bb2d6a6cbc7a70bdf0c53d238dc28e0b1d58Joseph Wen Cache.Entry cachePolicy = HttpHeaderParser.parseCacheHeaders(response); 1676a34bb2d6a6cbc7a70bdf0c53d238dc28e0b1d58Joseph Wen 1686a34bb2d6a6cbc7a70bdf0c53d238dc28e0b1d58Joseph Wen if (cachePolicy == null) { 1696a34bb2d6a6cbc7a70bdf0c53d238dc28e0b1d58Joseph Wen // Cache is disabled, set the expire time to 0. 1706a34bb2d6a6cbc7a70bdf0c53d238dc28e0b1d58Joseph Wen return DO_NOT_CACHE_RESULT; 1716a34bb2d6a6cbc7a70bdf0c53d238dc28e0b1d58Joseph Wen } else if (cachePolicy.ttl == 0) { 1726a34bb2d6a6cbc7a70bdf0c53d238dc28e0b1d58Joseph Wen // Cache policy is not specified, set the expire time to 0. 1736a34bb2d6a6cbc7a70bdf0c53d238dc28e0b1d58Joseph Wen return DO_NOT_CACHE_RESULT; 1746a34bb2d6a6cbc7a70bdf0c53d238dc28e0b1d58Joseph Wen } else { 1756a34bb2d6a6cbc7a70bdf0c53d238dc28e0b1d58Joseph Wen // cachePolicy.ttl is actually the expire timestamp in millisecond. 1766a34bb2d6a6cbc7a70bdf0c53d238dc28e0b1d58Joseph Wen return cachePolicy.ttl; 1776a34bb2d6a6cbc7a70bdf0c53d238dc28e0b1d58Joseph Wen } 1786a34bb2d6a6cbc7a70bdf0c53d238dc28e0b1d58Joseph Wen } 1796a34bb2d6a6cbc7a70bdf0c53d238dc28e0b1d58Joseph Wen 1806a34bb2d6a6cbc7a70bdf0c53d238dc28e0b1d58Joseph Wen /** 1816a34bb2d6a6cbc7a70bdf0c53d238dc28e0b1d58Joseph Wen * Converts an HTTP header map of the format provided by {@linkHttpUrlConnection} to a map of 1826a34bb2d6a6cbc7a70bdf0c53d238dc28e0b1d58Joseph Wen * the format accepted by {@link HttpHeaderParser}. It does this by joining all the entries for 1836a34bb2d6a6cbc7a70bdf0c53d238dc28e0b1d58Joseph Wen * a given header key with ", ". 1846a34bb2d6a6cbc7a70bdf0c53d238dc28e0b1d58Joseph Wen */ 1856a34bb2d6a6cbc7a70bdf0c53d238dc28e0b1d58Joseph Wen private Map<String, String> joinHttpHeaders(Map<String, List<String>> headers) { 1866a34bb2d6a6cbc7a70bdf0c53d238dc28e0b1d58Joseph Wen Map<String, String> joinedHeaders = new HashMap<String, String>(); 1876a34bb2d6a6cbc7a70bdf0c53d238dc28e0b1d58Joseph Wen for (Map.Entry<String, List<String>> entry : headers.entrySet()) { 1886a34bb2d6a6cbc7a70bdf0c53d238dc28e0b1d58Joseph Wen List<String> values = entry.getValue(); 1896a34bb2d6a6cbc7a70bdf0c53d238dc28e0b1d58Joseph Wen if (values.size() == 1) { 1906a34bb2d6a6cbc7a70bdf0c53d238dc28e0b1d58Joseph Wen joinedHeaders.put(entry.getKey(), values.get(0)); 1916a34bb2d6a6cbc7a70bdf0c53d238dc28e0b1d58Joseph Wen } else { 1926a34bb2d6a6cbc7a70bdf0c53d238dc28e0b1d58Joseph Wen joinedHeaders.put(entry.getKey(), Utils.joinStrings(", ", values)); 1936a34bb2d6a6cbc7a70bdf0c53d238dc28e0b1d58Joseph Wen } 1946a34bb2d6a6cbc7a70bdf0c53d238dc28e0b1d58Joseph Wen } 1956a34bb2d6a6cbc7a70bdf0c53d238dc28e0b1d58Joseph Wen return joinedHeaders; 1966a34bb2d6a6cbc7a70bdf0c53d238dc28e0b1d58Joseph Wen } 1976a34bb2d6a6cbc7a70bdf0c53d238dc28e0b1d58Joseph Wen} 198