blob: b25f5565f4b036a1eb9190aa8b5b37a3779c00ab [file] [log] [blame]
Julien Desprezd65e6912018-12-13 15:20:52 -08001/*
2 * Copyright (C) 2018 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 */
16package com.android.tradefed.config;
17
18import com.android.annotations.VisibleForTesting;
Julien Desprezd65e6912018-12-13 15:20:52 -080019import com.android.tradefed.config.OptionSetter.OptionFieldsForName;
Julien Desprez27bcd4b2018-12-19 12:49:08 -080020import com.android.tradefed.config.remote.GcsRemoteFileResolver;
21import com.android.tradefed.config.remote.IRemoteFileResolver;
Julien Desprezd65e6912018-12-13 15:20:52 -080022import com.android.tradefed.log.LogUtil.CLog;
23import com.android.tradefed.util.FileUtil;
Julien Despreze30f0d92019-01-07 17:32:14 -080024import com.android.tradefed.util.MultiMap;
Julien Desprezd65e6912018-12-13 15:20:52 -080025
26import java.io.File;
27import java.lang.reflect.Field;
28import java.util.ArrayList;
29import java.util.Collection;
Julien Desprez27bcd4b2018-12-19 12:49:08 -080030import java.util.HashMap;
Julien Desprezd65e6912018-12-13 15:20:52 -080031import java.util.HashSet;
Julien Desprez8f0432f2019-01-04 13:47:54 -080032import java.util.LinkedHashMap;
Julien Despreze30f0d92019-01-07 17:32:14 -080033import java.util.List;
Julien Desprezd65e6912018-12-13 15:20:52 -080034import java.util.Map;
Julien Desprez8f0432f2019-01-04 13:47:54 -080035import java.util.Map.Entry;
Julien Desprezd65e6912018-12-13 15:20:52 -080036import java.util.Set;
Julien Desprez15d9c7d2019-01-02 11:15:18 -080037import java.util.concurrent.atomic.AtomicBoolean;
Julien Desprezd65e6912018-12-13 15:20:52 -080038
39/**
40 * Class that helps resolving path to remote files.
41 *
42 * <p>For example: gs://bucket/path/file.txt will be resolved by downloading the file from the GCS
43 * bucket.
Julien Desprezd65e6912018-12-13 15:20:52 -080044 */
45public class DynamicRemoteFileResolver {
46
Julien Desprez15d9c7d2019-01-02 11:15:18 -080047 public static final String DYNAMIC_RESOLVER = "dynamic-resolver";
Julien Desprez27bcd4b2018-12-19 12:49:08 -080048 private static final Map<String, IRemoteFileResolver> PROTOCOL_SUPPORT = new HashMap<>();
49
50 static {
Julien Desprez27bcd4b2018-12-19 12:49:08 -080051 PROTOCOL_SUPPORT.put(GcsRemoteFileResolver.PROTOCOL, new GcsRemoteFileResolver());
52 }
Julien Desprez15d9c7d2019-01-02 11:15:18 -080053 // The configuration map being static, we only need to update it once per TF instance.
54 private static AtomicBoolean sIsUpdateDone = new AtomicBoolean(false);
Julien Desprez27bcd4b2018-12-19 12:49:08 -080055
Julien Desprezd65e6912018-12-13 15:20:52 -080056 private Map<String, OptionFieldsForName> mOptionMap;
57
58 /** Sets the map of options coming from {@link OptionSetter} */
59 public void setOptionMap(Map<String, OptionFieldsForName> optionMap) {
60 mOptionMap = optionMap;
61 }
62
63 /**
64 * Runs through all the {@link File} option type and check if their path should be resolved.
65 *
66 * @return The list of {@link File} that was resolved that way.
67 * @throws ConfigurationException
68 */
69 public final Set<File> validateRemoteFilePath() throws ConfigurationException {
70 Set<File> downloadedFiles = new HashSet<>();
71 try {
72 for (Map.Entry<String, OptionFieldsForName> optionPair : mOptionMap.entrySet()) {
Julien Desprezd65e6912018-12-13 15:20:52 -080073 final OptionFieldsForName optionFields = optionPair.getValue();
Julien Desprezd65e6912018-12-13 15:20:52 -080074 for (Map.Entry<Object, Field> fieldEntry : optionFields) {
75 final Object obj = fieldEntry.getKey();
76 final Field field = fieldEntry.getValue();
77 final Option option = field.getAnnotation(Option.class);
78 if (option == null) {
79 continue;
80 }
81 // At this point, we know this is an option field; make sure it's set
82 field.setAccessible(true);
83 final Object value;
84 try {
85 value = field.get(obj);
86 } catch (IllegalAccessException e) {
87 throw new ConfigurationException(
88 String.format("internal error: %s", e.getMessage()));
89 }
90
91 if (value == null) {
92 continue;
93 } else if (value instanceof File) {
94 File consideredFile = (File) value;
Julien Desprez27bcd4b2018-12-19 12:49:08 -080095 File downloadedFile = resolveRemoteFiles(consideredFile, option);
Julien Desprezd65e6912018-12-13 15:20:52 -080096 if (downloadedFile != null) {
97 downloadedFiles.add(downloadedFile);
98 // Replace the field value
99 try {
100 field.set(obj, downloadedFile);
101 } catch (IllegalAccessException e) {
102 CLog.e(e);
103 throw new ConfigurationException(
104 String.format(
Julien Desprez65249662018-12-27 16:50:29 -0800105 "Failed to download %s due to '%s'",
106 consideredFile.getPath(), e.getMessage()),
Julien Desprezd65e6912018-12-13 15:20:52 -0800107 e);
108 }
109 }
110 } else if (value instanceof Collection) {
111 Collection<Object> c = (Collection<Object>) value;
112 Collection<Object> copy = new ArrayList<>(c);
113 for (Object o : copy) {
114 if (o instanceof File) {
115 File consideredFile = (File) o;
Julien Desprez27bcd4b2018-12-19 12:49:08 -0800116 File downloadedFile = resolveRemoteFiles(consideredFile, option);
Julien Desprezd65e6912018-12-13 15:20:52 -0800117 if (downloadedFile != null) {
118 downloadedFiles.add(downloadedFile);
119 // TODO: See if order could be preserved.
120 c.remove(consideredFile);
121 c.add(downloadedFile);
122 }
123 }
124 }
Julien Desprez8f0432f2019-01-04 13:47:54 -0800125 } else if (value instanceof Map) {
126 Map<Object, Object> m = (Map<Object, Object>) value;
127 Map<Object, Object> copy = new LinkedHashMap<>(m);
128 for (Entry<Object, Object> entry : copy.entrySet()) {
129 Object key = entry.getKey();
130 Object val = entry.getValue();
131
132 Object finalKey = key;
133 Object finalVal = val;
134 if (key instanceof File) {
135 key = resolveRemoteFiles((File) key, option);
136 if (key != null) {
137 downloadedFiles.add((File) key);
138 finalKey = key;
139 }
140 }
141 if (val instanceof File) {
142 val = resolveRemoteFiles((File) val, option);
143 if (val != null) {
144 downloadedFiles.add((File) val);
145 finalVal = val;
146 }
147 }
148
149 m.remove(entry.getKey());
150 m.put(finalKey, finalVal);
151 }
Julien Despreze30f0d92019-01-07 17:32:14 -0800152 } else if (value instanceof MultiMap) {
153 MultiMap<Object, Object> m = (MultiMap<Object, Object>) value;
154 MultiMap<Object, Object> copy = new MultiMap<>(m);
155 for (Object key : copy.keySet()) {
156 List<Object> mapValues = copy.get(key);
157
158 m.remove(key);
159 Object finalKey = key;
160 if (key instanceof File) {
161 key = resolveRemoteFiles((File) key, option);
162 if (key != null) {
163 downloadedFiles.add((File) key);
164 finalKey = key;
165 }
166 }
167 for (Object mapValue : mapValues) {
168 if (mapValue instanceof File) {
169 File f = resolveRemoteFiles((File) mapValue, option);
170 if (f != null) {
171 downloadedFiles.add(f);
172 mapValue = f;
173 }
174 }
175 m.put(finalKey, mapValue);
176 }
177 }
Julien Desprezd65e6912018-12-13 15:20:52 -0800178 }
Julien Desprezd65e6912018-12-13 15:20:52 -0800179 }
180 }
181 } catch (ConfigurationException e) {
182 // Clean up the files before throwing
183 for (File f : downloadedFiles) {
184 FileUtil.recursiveDelete(f);
185 }
186 throw e;
187 }
188 return downloadedFiles;
189 }
190
Julien Desprezd65e6912018-12-13 15:20:52 -0800191 @VisibleForTesting
Julien Desprez9b477232019-03-07 11:08:25 -0800192 protected IRemoteFileResolver getResolver(String protocol) {
Julien Desprez15d9c7d2019-01-02 11:15:18 -0800193 if (updateProtocols()) {
194 IGlobalConfiguration globalConfig = getGlobalConfig();
195 Object o = globalConfig.getConfigurationObject(DYNAMIC_RESOLVER);
196 if (o != null) {
197 if (o instanceof IRemoteFileResolver) {
198 IRemoteFileResolver resolver = (IRemoteFileResolver) o;
199 CLog.d("Adding %s to supported remote file resolver", resolver);
200 PROTOCOL_SUPPORT.put(resolver.getSupportedProtocol(), resolver);
201 } else {
202 CLog.e("%s is not of type IRemoteFileResolver", o);
203 }
204 }
205 }
Julien Desprez27bcd4b2018-12-19 12:49:08 -0800206 return PROTOCOL_SUPPORT.get(protocol);
Julien Desprezd65e6912018-12-13 15:20:52 -0800207 }
208
Julien Desprez15d9c7d2019-01-02 11:15:18 -0800209 @VisibleForTesting
Julien Desprez9b477232019-03-07 11:08:25 -0800210 protected boolean updateProtocols() {
Julien Desprez15d9c7d2019-01-02 11:15:18 -0800211 return sIsUpdateDone.compareAndSet(false, true);
212 }
213
214 @VisibleForTesting
215 IGlobalConfiguration getGlobalConfig() {
216 return GlobalConfiguration.getInstance();
217 }
218
Julien Desprez27bcd4b2018-12-19 12:49:08 -0800219 private File resolveRemoteFiles(File consideredFile, Option option)
Julien Desprezd65e6912018-12-13 15:20:52 -0800220 throws ConfigurationException {
Julien Desprez27bcd4b2018-12-19 12:49:08 -0800221 String path = consideredFile.getPath();
222 String protocol = getProtocol(path);
223 IRemoteFileResolver resolver = getResolver(protocol);
224 if (resolver != null) {
225 return resolver.resolveRemoteFiles(consideredFile, option);
Julien Desprezd65e6912018-12-13 15:20:52 -0800226 }
Julien Desprez27bcd4b2018-12-19 12:49:08 -0800227 // Not a remote file
Julien Desprezd65e6912018-12-13 15:20:52 -0800228 return null;
229 }
Julien Desprez27bcd4b2018-12-19 12:49:08 -0800230
231 /**
232 * Java URL doesn't recognize 'gs' as a protocol and throws an exception so we do the protocol
233 * extraction ourselves.
234 */
235 private String getProtocol(String path) {
236 int index = path.indexOf(":/");
237 if (index == -1) {
238 return "";
239 }
240 return path.substring(0, index);
241 }
Julien Desprezd65e6912018-12-13 15:20:52 -0800242}