1 /* 2 * Copyright 2022 Google LLC 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 package com.google.android.libraries.mobiledatadownload.downloader; 17 18 import android.net.Uri; 19 import androidx.annotation.VisibleForTesting; 20 import com.google.android.libraries.mobiledatadownload.DownloadException; 21 import com.google.android.libraries.mobiledatadownload.DownloadException.DownloadResultCode; 22 import com.google.android.libraries.mobiledatadownload.internal.logging.LogUtil; 23 import com.google.common.base.Preconditions; 24 import com.google.common.collect.ImmutableMap; 25 import com.google.common.util.concurrent.Futures; 26 import com.google.common.util.concurrent.ListenableFuture; 27 import com.google.errorprone.annotations.CanIgnoreReturnValue; 28 import com.google.errorprone.annotations.CheckReturnValue; 29 import java.net.MalformedURLException; 30 import java.util.HashMap; 31 import java.util.Map; 32 33 /** 34 * A composite {@link FileDownloader} that delegates to specific registered FileDownloaders based on 35 * URL scheme. 36 */ 37 public final class MultiSchemeFileDownloader implements FileDownloader { 38 private static final String TAG = "MultiSchemeFileDownloader"; 39 40 /** Builder for {@link MultiSchemeFileDownloader}. */ 41 public static final class Builder { 42 private final Map<String, FileDownloader> schemeToDownloader = new HashMap<>(); 43 44 /** Associates a url scheme (e.g. "http") with a specific {@link FileDownloader} delegate. */ 45 @CanIgnoreReturnValue addScheme(String scheme, FileDownloader downloader)46 public MultiSchemeFileDownloader.Builder addScheme(String scheme, FileDownloader downloader) { 47 schemeToDownloader.put( 48 Preconditions.checkNotNull(scheme), Preconditions.checkNotNull(downloader)); 49 return this; 50 } 51 build()52 public MultiSchemeFileDownloader build() { 53 return new MultiSchemeFileDownloader(this); 54 } 55 } 56 57 private final ImmutableMap<String, FileDownloader> schemeToDownloader; 58 59 /** Returns a Builder for {@link MultiSchemeFileDownloader}. */ builder()60 public static Builder builder() { 61 return new Builder(); 62 } 63 64 /** Returns a Builder containing all registered FileDownloaders. */ toBuilder()65 public Builder toBuilder() { 66 final Builder builder = new Builder(); 67 for (Map.Entry<String, FileDownloader> entry : schemeToDownloader.entrySet()) { 68 builder.addScheme(entry.getKey(), entry.getValue()); 69 } 70 return builder; 71 } 72 73 /** Returns true if a FileDownloader is registered for the given scheme. */ supportsScheme(String scheme)74 public boolean supportsScheme(String scheme) { 75 return schemeToDownloader.containsKey(scheme); 76 } 77 MultiSchemeFileDownloader(Builder builder)78 private MultiSchemeFileDownloader(Builder builder) { 79 this.schemeToDownloader = ImmutableMap.copyOf(builder.schemeToDownloader); 80 } 81 82 @Override 83 @CheckReturnValue startDownloading(DownloadRequest downloadRequest)84 public ListenableFuture<Void> startDownloading(DownloadRequest downloadRequest) { 85 FileDownloader delegate; 86 try { 87 delegate = getDelegate(downloadRequest.urlToDownload()); 88 } catch (DownloadException e) { 89 return Futures.immediateFailedFuture(e); 90 } 91 return delegate.startDownloading(downloadRequest); 92 } 93 94 @Override 95 @CheckReturnValue isContentChanged( CheckContentChangeRequest checkContentChangeRequest)96 public ListenableFuture<CheckContentChangeResponse> isContentChanged( 97 CheckContentChangeRequest checkContentChangeRequest) { 98 FileDownloader delegate; 99 try { 100 delegate = getDelegate(checkContentChangeRequest.url()); 101 } catch (DownloadException e) { 102 return Futures.immediateFailedFuture(e); 103 } 104 return delegate.isContentChanged(checkContentChangeRequest); 105 } 106 107 /** Extract the scheme of a url string. */ 108 @VisibleForTesting getScheme(String url)109 static String getScheme(String url) throws MalformedURLException { 110 Uri parsed = Uri.parse(url); 111 if (parsed == null) { 112 throw new MalformedURLException("Could not parse URL."); 113 } 114 String scheme = parsed.getScheme(); 115 if (scheme == null) { 116 throw new MalformedURLException("URL contained no scheme."); 117 } 118 return scheme; 119 } 120 121 /** 122 * Lookup the delegate FileDownloader that can handle a url, based on the url's scheme. 123 * 124 * @throws DownloadException If an appropriate delegate FileDownloader could not be found. 125 */ getDelegate(String url)126 FileDownloader getDelegate(String url) throws DownloadException { 127 String scheme; 128 try { 129 scheme = getScheme(url); 130 } catch (MalformedURLException e) { 131 LogUtil.e("%s: The download url is malformed, url = %s", TAG, url); 132 throw DownloadException.builder() 133 .setDownloadResultCode(DownloadResultCode.MALFORMED_DOWNLOAD_URL) 134 .setCause(e) 135 .build(); 136 } 137 138 FileDownloader downloader = schemeToDownloader.get(scheme); 139 if (downloader == null) { 140 LogUtil.e( 141 "%s: No registered downloader supports the download url scheme, scheme = %s", 142 TAG, scheme); 143 throw DownloadException.builder() 144 .setDownloadResultCode(DownloadResultCode.UNSUPPORTED_DOWNLOAD_URL_SCHEME) 145 .build(); 146 } 147 return downloader; 148 } 149 } 150