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