1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16 package com.linecorp.centraldogma.client;
17
18 import static com.google.common.base.Preconditions.checkArgument;
19 import static com.google.common.base.Preconditions.checkNotNull;
20 import static com.google.common.base.Preconditions.checkState;
21 import static java.util.Objects.requireNonNull;
22
23 import java.io.IOException;
24 import java.net.InetSocketAddress;
25 import java.net.URI;
26 import java.net.URL;
27 import java.time.Duration;
28 import java.util.ArrayList;
29 import java.util.Collections;
30 import java.util.Enumeration;
31 import java.util.HashMap;
32 import java.util.List;
33 import java.util.Map;
34 import java.util.Set;
35 import java.util.concurrent.TimeUnit;
36
37 import javax.annotation.Nullable;
38
39 import com.fasterxml.jackson.core.type.TypeReference;
40 import com.fasterxml.jackson.databind.ObjectMapper;
41 import com.google.common.collect.ImmutableList;
42 import com.google.common.collect.ImmutableSet;
43 import com.google.common.collect.Iterables;
44 import com.google.common.net.InetAddresses;
45
46 import com.linecorp.centraldogma.common.RevisionNotFoundException;
47 import com.linecorp.centraldogma.internal.CsrfToken;
48
49
50
51
52 public abstract class AbstractCentralDogmaBuilder<B extends AbstractCentralDogmaBuilder<B>> {
53
54 private static final String TEST_PROFILE_RESOURCE_PATH = "centraldogma-profiles-test.json";
55 private static final String PROFILE_RESOURCE_PATH = "centraldogma-profiles.json";
56 private static final List<String> DEFAULT_PROFILE_RESOURCE_PATHS =
57 ImmutableList.of(TEST_PROFILE_RESOURCE_PATH, PROFILE_RESOURCE_PATH);
58
59 static final int DEFAULT_PORT = 36462;
60
61 private static final int DEFAULT_MAX_NUM_RETRIES_ON_REPLICATION_LAG = 5;
62 private static final int DEFAULT_RETRY_INTERVAL_ON_REPLICATION_LAG_SECONDS = 2;
63
64 private ImmutableSet<InetSocketAddress> hosts = ImmutableSet.of();
65 private boolean useTls;
66 private List<String> profileResourcePaths = DEFAULT_PROFILE_RESOURCE_PATHS;
67 @Nullable
68 private String selectedProfile;
69 private String accessToken = CsrfToken.ANONYMOUS;
70 private int maxNumRetriesOnReplicationLag = DEFAULT_MAX_NUM_RETRIES_ON_REPLICATION_LAG;
71 private long retryIntervalOnReplicationLagMillis =
72 TimeUnit.SECONDS.toMillis(DEFAULT_RETRY_INTERVAL_ON_REPLICATION_LAG_SECONDS);
73
74
75
76
77 @SuppressWarnings("unchecked")
78 protected final B self() {
79 return (B) this;
80 }
81
82
83
84
85
86
87
88
89
90 @Deprecated
91 public final B uri(String uri) {
92 final URI parsed = URI.create(requireNonNull(uri, "uri"));
93 final String host = parsed.getHost();
94 final int port = parsed.getPort();
95 checkArgument(host != null, "uri: %s (must contain a host part)", uri);
96 if (port < 0) {
97 host(host);
98 } else {
99 host(host, port);
100 }
101 return self();
102 }
103
104
105
106
107
108
109
110 public final B host(String host) {
111 return host(host, DEFAULT_PORT);
112 }
113
114
115
116
117
118
119
120 public final B host(String host, int port) {
121 requireNonNull(host, "host");
122 checkArgument(!host.startsWith("group:"), "host: %s (must not start with 'group:')", host);
123 checkArgument(port >= 1 && port < 65536, "port: %s (expected: 1 .. 65535)", port);
124
125 final InetSocketAddress addr = newEndpoint(host, port);
126 checkState(selectedProfile == null, "profile() and host() cannot be used together.");
127 hosts = ImmutableSet.<InetSocketAddress>builder().addAll(hosts).add(addr).build();
128 return self();
129 }
130
131
132
133
134 public final B useTls() {
135 return useTls(true);
136 }
137
138
139
140
141 public final B useTls(boolean useTls) {
142 checkState(selectedProfile == null, "useTls() cannot be called once a profile is selected.");
143 this.useTls = useTls;
144 return self();
145 }
146
147
148
149
150
151
152 protected final boolean isUseTls() {
153 return useTls;
154 }
155
156
157
158
159
160
161
162
163 public final B profileResources(String... paths) {
164 return profileResources(ImmutableList.copyOf(requireNonNull(paths, "paths")));
165 }
166
167
168
169
170
171
172
173
174 public final B profileResources(Iterable<String> paths) {
175 final List<String> newPaths = ImmutableList.copyOf(requireNonNull(paths, "paths"));
176 checkArgument(!newPaths.isEmpty(), "paths is empty.");
177 checkState(selectedProfile == null, "profileResources cannot be set after profile() is called.");
178 profileResourcePaths = newPaths;
179 return self();
180 }
181
182
183
184
185
186
187
188
189
190
191
192 public final B profile(String... profiles) {
193 requireNonNull(profiles, "profiles");
194 return profile(ImmutableList.copyOf(profiles));
195 }
196
197
198
199
200
201
202
203
204
205
206
207 public final B profile(ClassLoader classLoader, String... profiles) {
208 requireNonNull(profiles, "profiles");
209 return profile(classLoader, ImmutableList.copyOf(profiles));
210 }
211
212
213
214
215
216
217
218
219
220
221
222 public final B profile(Iterable<String> profiles) {
223 final ClassLoader ccl = Thread.currentThread().getContextClassLoader();
224 if (ccl != null) {
225 profile(ccl, profiles);
226 } else {
227 profile(getClass().getClassLoader(), profiles);
228 }
229 return self();
230 }
231
232
233
234
235
236
237
238
239
240
241
242 public final B profile(ClassLoader classLoader, Iterable<String> profiles) {
243 requireNonNull(classLoader, "classLoader");
244 requireNonNull(profiles, "profiles");
245 checkState(selectedProfile == null, "profile cannot be loaded more than once.");
246 checkState(hosts.isEmpty(), "profile() and host() cannot be used together.");
247
248 final Map<String, ClientProfile> availableProfiles = new HashMap<>();
249 try {
250 final List<URL> resourceUrls = findProfileResources(classLoader);
251 checkState(!resourceUrls.isEmpty(), "failed to find any of: ", profileResourcePaths);
252
253 for (URL resourceUrl : resourceUrls) {
254 final List<ClientProfile> availableProfileList =
255 new ObjectMapper().readValue(resourceUrl, new TypeReference<List<ClientProfile>>() {});
256
257
258 availableProfileList.forEach(profile -> {
259 final String name = profile.name();
260 final ClientProfile existingProfile = availableProfiles.get(name);
261 if (existingProfile == null || existingProfile.priority() < profile.priority()) {
262
263 availableProfiles.put(name, profile);
264 }
265 });
266 }
267 } catch (IOException e) {
268 throw new IllegalStateException("failed to load: " + PROFILE_RESOURCE_PATH, e);
269 }
270
271 final List<String> reversedProfiles = reverse(profiles);
272 checkArgument(!reversedProfiles.isEmpty(), "profiles is empty.");
273 for (String candidateName : reversedProfiles) {
274 checkNotNull(candidateName, "profiles contains null: %s", profiles);
275
276 final ClientProfile candidate = availableProfiles.get(candidateName);
277 if (candidate == null) {
278 continue;
279 }
280
281 final ImmutableSet.Builder<InetSocketAddress> newHostsBuilder = ImmutableSet.builder();
282 candidate.hosts().stream()
283 .filter(e -> (useTls ? "https" : "http").equals(e.protocol()))
284 .forEach(e -> newHostsBuilder.add(newEndpoint(e.host(), e.port())));
285
286 final ImmutableSet<InetSocketAddress> newHosts = newHostsBuilder.build();
287 if (!newHosts.isEmpty()) {
288 selectedProfile = candidateName;
289 hosts = newHosts;
290 return self();
291 }
292 }
293
294 throw new IllegalArgumentException("no profile matches: " + profiles);
295 }
296
297 private List<URL> findProfileResources(ClassLoader classLoader) throws IOException {
298 final ImmutableList.Builder<URL> urls = ImmutableList.builder();
299 for (String p : profileResourcePaths) {
300 for (final Enumeration<URL> e = classLoader.getResources(p); e.hasMoreElements();) {
301 urls.add(e.nextElement());
302 }
303 }
304 return urls.build();
305 }
306
307 private static List<String> reverse(Iterable<String> profiles) {
308 final List<String> reversedProfiles = new ArrayList<>();
309 Iterables.addAll(reversedProfiles, profiles);
310 Collections.reverse(reversedProfiles);
311 return reversedProfiles;
312 }
313
314 private static InetSocketAddress newEndpoint(String host, int port) {
315 final InetSocketAddress endpoint;
316 if (InetAddresses.isInetAddress(host)) {
317 endpoint = new InetSocketAddress(InetAddresses.forString(host), port);
318 } else {
319 endpoint = InetSocketAddress.createUnresolved(host, port);
320 }
321 return endpoint;
322 }
323
324
325
326
327
328
329 @Nullable
330 protected final String selectedProfile() {
331 return selectedProfile;
332 }
333
334
335
336
337 protected final Set<InetSocketAddress> hosts() {
338 return hosts;
339 }
340
341
342
343
344 public final B accessToken(String accessToken) {
345 requireNonNull(accessToken, "accessToken");
346 checkArgument(!accessToken.isEmpty(), "accessToken is empty.");
347 this.accessToken = accessToken;
348 return self();
349 }
350
351
352
353
354 protected String accessToken() {
355 return accessToken;
356 }
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375 public final B maxNumRetriesOnReplicationLag(int maxRetriesOnReplicationLag) {
376 checkArgument(maxRetriesOnReplicationLag >= 0,
377 "maxRetriesOnReplicationLag: %s (expected: >= 0)", maxRetriesOnReplicationLag);
378 this.maxNumRetriesOnReplicationLag = maxRetriesOnReplicationLag;
379 return self();
380 }
381
382
383
384
385 protected int maxNumRetriesOnReplicationLag() {
386 return maxNumRetriesOnReplicationLag;
387 }
388
389
390
391
392
393 public final B retryIntervalOnReplicationLag(Duration retryIntervalOnReplicationLag) {
394 requireNonNull(retryIntervalOnReplicationLag, "retryIntervalOnReplicationLag");
395 checkArgument(!retryIntervalOnReplicationLag.isNegative(),
396 "retryIntervalOnReplicationLag: %s (expected: >= 0)", retryIntervalOnReplicationLag);
397 return retryIntervalOnReplicationLagMillis(retryIntervalOnReplicationLag.toMillis());
398 }
399
400
401
402
403
404 public final B retryIntervalOnReplicationLagMillis(long retryIntervalOnReplicationLagMillis) {
405 checkArgument(retryIntervalOnReplicationLagMillis >= 0,
406 "retryIntervalOnReplicationLagMillis: %s (expected: >= 0)",
407 retryIntervalOnReplicationLagMillis);
408 this.retryIntervalOnReplicationLagMillis = retryIntervalOnReplicationLagMillis;
409 return self();
410 }
411
412
413
414
415 protected long retryIntervalOnReplicationLagMillis() {
416 return retryIntervalOnReplicationLagMillis;
417 }
418 }