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