1 package io.getunleash.util;
2
3 import static io.getunleash.DefaultUnleash.UNKNOWN_STRATEGY;
4
5 import io.getunleash.CustomHttpHeadersProvider;
6 import io.getunleash.DefaultCustomHttpHeadersProviderImpl;
7 import io.getunleash.UnleashContextProvider;
8 import io.getunleash.UnleashException;
9 import io.getunleash.event.NoOpSubscriber;
10 import io.getunleash.event.UnleashSubscriber;
11 import io.getunleash.lang.Nullable;
12 import io.getunleash.metric.DefaultHttpMetricsSender;
13 import io.getunleash.repository.HttpFeatureFetcher;
14 import io.getunleash.repository.ToggleBootstrapProvider;
15 import io.getunleash.strategy.Strategy;
16 import java.io.File;
17 import java.math.BigInteger;
18 import java.net.*;
19 import java.nio.charset.StandardCharsets;
20 import java.security.MessageDigest;
21 import java.security.NoSuchAlgorithmException;
22 import java.time.Duration;
23 import java.util.HashMap;
24 import java.util.Map;
25 import java.util.Optional;
26 import java.util.UUID;
27 import java.util.function.Consumer;
28
29 public class UnleashConfig {
30
31     public static final String LEGACY_UNLEASH_APP_NAME_HEADER = "UNLEASH-APPNAME";
32     public static final String UNLEASH_INSTANCE_ID_HEADER = "UNLEASH-INSTANCEID";
33     public static final String UNLEASH_CONNECTION_ID_HEADER = "X-UNLEASH-CONNECTION-ID";
34     public static final String UNLEASH_APP_NAME_HEADER = "X-UNLEASH-APPNAME";
35     public static final String UNLEASH_SDK_HEADER = "X-UNLEASH-SDK";
36
37     private final URI unleashAPI;
38     private final UnleashURLs unleashURLs;
39     private final Map<String, String> customHttpHeaders;
40     private final CustomHttpHeadersProvider customHttpHeadersProvider;
41     private final String appName;
42     private final String environment;
43     private final String instanceId;
44     private final String connectionId;
45     private final String sdkVersion;
46     private final String backupFile;
47
48     private final String clientSpecificationVersion;
49     @Nullable private final String projectName;
50     @Nullable private final String namePrefix;
51     private final long fetchTogglesInterval;
52
53     private final Duration fetchTogglesConnectTimeout;
54
55     private final Duration fetchTogglesReadTimeout;
56
57     private final boolean disablePolling;
58     private final long sendMetricsInterval;
59
60     private final Duration sendMetricsConnectTimeout;
61
62     private final Duration sendMetricsReadTimeout;
63     private final boolean disableMetrics;
64     private final boolean isProxyAuthenticationByJvmProperties;
65     private final UnleashFeatureFetcherFactory unleashFeatureFetcherFactory;
66
67     private final MetricSenderFactory metricSenderFactory;
68
69     private final UnleashContextProvider contextProvider;
70     private final boolean synchronousFetchOnInitialisation;
71     private final UnleashScheduledExecutor unleashScheduledExecutor;
72     private final UnleashSubscriber unleashSubscriber;
73     @Nullable private final Strategy fallbackStrategy;
74     @Nullable private final ToggleBootstrapProvider toggleBootstrapProvider;
75     @Nullable private final Proxy proxy;
76     @Nullable private final Consumer<UnleashException> startupExceptionHandler;
77
78     private UnleashConfig(
79             @Nullable URI unleashAPI,
80             Map<String, String> customHttpHeaders,
81             CustomHttpHeadersProvider customHttpHeadersProvider,
82             @Nullable String appName,
83             String environment,
84             @Nullable String instanceId,
85             String connectionId,
86             String sdkVersion,
87             String backupFile,
88             @Nullable String projectName,
89             @Nullable String namePrefix,
90             long fetchTogglesInterval,
91             Duration fetchTogglesConnectTimeout,
92             Duration fetchTogglesReadTimeout,
93             boolean disablePolling,
94             long sendMetricsInterval,
95             Duration sendMetricsConnectTimeout,
96             Duration sendMetricsReadTimeout,
97             boolean disableMetrics,
98             UnleashContextProvider contextProvider,
99             boolean isProxyAuthenticationByJvmProperties,
100             boolean synchronousFetchOnInitialisation,
101             UnleashFeatureFetcherFactory unleashFeatureFetcherFactory,
102             MetricSenderFactory metricSenderFactory,
103             @Nullable UnleashScheduledExecutor unleashScheduledExecutor,
104             @Nullable UnleashSubscriber unleashSubscriber,
105             @Nullable Strategy fallbackStrategy,
106             @Nullable ToggleBootstrapProvider unleashBootstrapProvider,
107             @Nullable Proxy proxy,
108             @Nullable Authenticator proxyAuthenticator,
109             @Nullable Consumer<UnleashException> startupExceptionHandler) {
110
111         if (appName == null) {
112             throw new IllegalStateException("You are required to specify the unleash appName");
113         }
114
115         if (instanceId == null) {
116             throw new IllegalStateException("You are required to specify the unleash instanceId");
117         }
118
119         if (unleashAPI == null) {
120             throw new IllegalStateException("You are required to specify the unleashAPI url");
121         }
122
123         if (unleashScheduledExecutor == null) {
124             throw new IllegalStateException("You are required to specify a scheduler");
125         }
126
127         if (unleashSubscriber == null) {
128             throw new IllegalStateException("You are required to specify a subscriber");
129         }
130
131         if (fallbackStrategy == null) {
132             this.fallbackStrategy = UNKNOWN_STRATEGY;
133         } else {
134             this.fallbackStrategy = fallbackStrategy;
135         }
136
137         if (isProxyAuthenticationByJvmProperties && proxyAuthenticator == null) {
138             enableProxyAuthentication();
139         } else if (proxyAuthenticator != null) {
140             Authenticator.setDefault(proxyAuthenticator);
141         }
142
143         this.unleashAPI = unleashAPI;
144         this.customHttpHeaders = customHttpHeaders;
145         this.customHttpHeadersProvider = customHttpHeadersProvider;
146         this.unleashURLs = new UnleashURLs(unleashAPI);
147         this.appName = appName;
148         this.environment = environment;
149         this.instanceId = instanceId;
150         this.connectionId = connectionId;
151         this.sdkVersion = sdkVersion;
152         this.backupFile = backupFile;
153         this.projectName = projectName;
154         this.namePrefix = namePrefix;
155         this.fetchTogglesInterval = fetchTogglesInterval;
156         this.fetchTogglesConnectTimeout = fetchTogglesConnectTimeout;
157         this.fetchTogglesReadTimeout = fetchTogglesReadTimeout;
158         this.disablePolling = disablePolling;
159         this.sendMetricsInterval = sendMetricsInterval;
160         this.sendMetricsConnectTimeout = sendMetricsConnectTimeout;
161         this.sendMetricsReadTimeout = sendMetricsReadTimeout;
162         this.disableMetrics = disableMetrics;
163         this.contextProvider = contextProvider;
164         this.isProxyAuthenticationByJvmProperties = isProxyAuthenticationByJvmProperties;
165         this.synchronousFetchOnInitialisation = synchronousFetchOnInitialisation;
166         this.unleashScheduledExecutor = unleashScheduledExecutor;
167         this.unleashSubscriber = unleashSubscriber;
168         this.toggleBootstrapProvider = unleashBootstrapProvider;
169         this.proxy = proxy;
170         this.unleashFeatureFetcherFactory = unleashFeatureFetcherFactory;
171         this.metricSenderFactory = metricSenderFactory;
172         this.clientSpecificationVersion =
173                 UnleashProperties.getProperty("client.specification.version");
174         this.startupExceptionHandler = startupExceptionHandler;
175     }
176
177     public static Builder builder() {
178         return new Builder();
179     }
180
181     public static void setRequestProperties(HttpURLConnection connection, UnleashConfig config) {
182         connection.setRequestProperty(LEGACY_UNLEASH_APP_NAME_HEADER, config.getAppName());
183         connection.setRequestProperty(UNLEASH_APP_NAME_HEADER, config.getAppName());
184         connection.setRequestProperty(UNLEASH_INSTANCE_ID_HEADER, config.getInstanceId());
185         connection.setRequestProperty(UNLEASH_CONNECTION_ID_HEADER, config.getConnectionId());
186         connection.setRequestProperty(UNLEASH_SDK_HEADER, config.getSdkVersion());
187         connection.setRequestProperty("User-Agent", config.getAppName());
188         connection.setRequestProperty(
189                 "Unleash-Client-Spec", config.getClientSpecificationVersion());
190         config.getCustomHttpHeaders().forEach(connection::setRequestProperty);
191         config.customHttpHeadersProvider.getCustomHeaders().forEach(connection::setRequestProperty);
192     }
193
194     private void enableProxyAuthentication() {
195         // http.proxyUser http.proxyPassword is only consumed by Apache HTTP Client, for
196         // HttpUrlConnection we have to define an Authenticator
197         Authenticator.setDefault(new SystemProxyAuthenticator());
198     }
199
200     public URI getUnleashAPI() {
201         return unleashAPI;
202     }
203
204     public Map<String, String> getCustomHttpHeaders() {
205         return customHttpHeaders;
206     }
207
208     public CustomHttpHeadersProvider getCustomHttpHeadersProvider() {
209         return customHttpHeadersProvider;
210     }
211
212     public String getAppName() {
213         return appName;
214     }
215
216     public String getEnvironment() {
217         return environment;
218     }
219
220     public String getInstanceId() {
221         return instanceId;
222     }
223
224     String getConnectionId() {
225         return connectionId;
226     }
227
228     public String getSdkVersion() {
229         return sdkVersion;
230     }
231
232     public String getClientSpecificationVersion() {
233         return clientSpecificationVersion;
234     }
235
236     public @Nullable String getProjectName() {
237         return projectName;
238     }
239
240     public long getFetchTogglesInterval() {
241         return fetchTogglesInterval;
242     }
243
244     public Duration getFetchTogglesConnectTimeout() {
245         return fetchTogglesConnectTimeout;
246     }
247
248     public Duration getFetchTogglesReadTimeout() {
249         return fetchTogglesReadTimeout;
250     }
251
252     public boolean isDisablePolling() {
253         return disablePolling;
254     }
255
256     public long getSendMetricsInterval() {
257         return sendMetricsInterval;
258     }
259
260     public Duration getSendMetricsConnectTimeout() {
261         return sendMetricsConnectTimeout;
262     }
263
264     public Duration getSendMetricsReadTimeout() {
265         return sendMetricsReadTimeout;
266     }
267
268     public UnleashURLs getUnleashURLs() {
269         return unleashURLs;
270     }
271
272     public boolean isDisableMetrics() {
273         return disableMetrics;
274     }
275
276     public String getBackupFile() {
277         return this.backupFile;
278     }
279
280     @Nullable
281     public String getApiKey() {
282         String auth = this.customHttpHeadersProvider.getCustomHeaders().get("Authorization");
283         if (auth == null) {
284             auth = this.customHttpHeaders.get("Authorization");
285         }
286         return auth;
287     }
288
289     public String getClientIdentifier() {
290         try {
291             MessageDigest md = MessageDigest.getInstance("SHA-256");
292             if (getApiKey() != null) {
293                 md.update(getApiKey().getBytes(StandardCharsets.UTF_8));
294             }
295             md.update(getAppName().getBytes(StandardCharsets.UTF_8));
296             md.update(getInstanceId().getBytes(StandardCharsets.UTF_8));
297             return new BigInteger(1, md.digest()).toString(16);
298         } catch (NoSuchAlgorithmException nse) {
299             throw new IllegalStateException("Could not build hash for client", nse);
300         }
301     }
302
303     public boolean isSynchronousFetchOnInitialisation() {
304         return synchronousFetchOnInitialisation;
305     }
306
307     public UnleashContextProvider getContextProvider() {
308         return contextProvider;
309     }
310
311     public UnleashScheduledExecutor getScheduledExecutor() {
312         return unleashScheduledExecutor;
313     }
314
315     public UnleashSubscriber getSubscriber() {
316         return unleashSubscriber;
317     }
318
319     public boolean isProxyAuthenticationByJvmProperties() {
320         return isProxyAuthenticationByJvmProperties;
321     }
322
323     @Nullable
324     public Strategy getFallbackStrategy() {
325         return fallbackStrategy;
326     }
327
328     @Nullable
329     public ToggleBootstrapProvider getToggleBootstrapProvider() {
330         return toggleBootstrapProvider;
331     }
332
333     @Nullable
334     public String getNamePrefix() {
335         return namePrefix;
336     }
337
338     @Nullable
339     public Proxy getProxy() {
340         return proxy;
341     }
342
343     public MetricSenderFactory getMetricSenderFactory() {
344         return this.metricSenderFactory;
345     }
346
347     public UnleashFeatureFetcherFactory getUnleashFeatureFetcherFactory() {
348         return this.unleashFeatureFetcherFactory;
349     }
350
351     @Nullable
352     public Consumer<UnleashException> getStartupExceptionHandler() {
353         return startupExceptionHandler;
354     }
355
356     static class SystemProxyAuthenticator extends Authenticator {
357         @Override
358         protected @Nullable PasswordAuthentication getPasswordAuthentication() {
359             if (getRequestorType() == RequestorType.PROXY) {
360                 final String proto = getRequestingProtocol().toLowerCase();
361                 final String proxyHost = System.getProperty(proto + ".proxyHost""");
362                 final String proxyPort = System.getProperty(proto + ".proxyPort""");
363                 final String proxyUser = System.getProperty(proto + ".proxyUser""");
364                 final String proxyPassword = System.getProperty(proto + ".proxyPassword""");
365
366                 // Only apply PasswordAuthentication to requests to the proxy itself - if not set
367                 // just ignore
368                 if (getRequestingHost().equalsIgnoreCase(proxyHost)
369                         && Integer.parseInt(proxyPort) == getRequestingPort()) {
370                     return new PasswordAuthentication(proxyUser, proxyPassword.toCharArray());
371                 }
372             }
373             return null;
374         }
375     }
376
377     static class CustomProxyAuthenticator extends Authenticator {
378
379         private final Proxy proxy;
380         private final String proxyUser;
381         private final String proxyPassword;
382
383         public CustomProxyAuthenticator(Proxy proxy, String proxyUser, String proxyPassword) {
384             this.proxy = proxy;
385             this.proxyUser = proxyUser;
386             this.proxyPassword = proxyPassword;
387         }
388
389         @Override
390         protected @Nullable PasswordAuthentication getPasswordAuthentication() {
391             if (getRequestorType() == RequestorType.PROXY
392                     && proxy.type() == Proxy.Type.HTTP
393                     && proxy.address() instanceof InetSocketAddress) {
394                 final String proxyHost = ((InetSocketAddress) proxy.address()).getHostName();
395                 final int proxyPort = ((InetSocketAddress) proxy.address()).getPort();
396
397                 // Only apply PasswordAuthentication to requests to the proxy
398                 // itself - if not set
399                 // just ignore
400                 if (getRequestingHost().equalsIgnoreCase(proxyHost)
401                         && proxyPort == getRequestingPort()) {
402                     return new PasswordAuthentication(proxyUser, proxyPassword.toCharArray());
403                 }
404             }
405             return null;
406         }
407     }
408
409     public static class Builder {
410
411         private @Nullable URI unleashAPI;
412         private Map<String, String> customHttpHeaders = new HashMap<>();
413         private CustomHttpHeadersProvider customHttpHeadersProvider =
414                 new DefaultCustomHttpHeadersProviderImpl();
415         private @Nullable String appName;
416         private String environment = "default";
417         private String instanceId = getDefaultInstanceId();
418         private String connectionId = getDefaultConnectionId();
419         private final String sdkVersion = getDefaultSdkVersion();
420         private @Nullable String backupFile;
421         private @Nullable String projectName;
422         private @Nullable String namePrefix;
423         private long fetchTogglesInterval = 10;
424
425         private Duration fetchTogglesConnectTimeout = Duration.ofSeconds(10);
426
427         private Duration fetchTogglesReadTimeout = Duration.ofSeconds(10);
428
429         private boolean disablePolling = false;
430         private long sendMetricsInterval = 60;
431
432         private Duration sendMetricsConnectTimeout = Duration.ofSeconds(10);
433
434         private Duration sendMetricsReadTimeout = Duration.ofSeconds(10);
435         private boolean disableMetrics = false;
436         private UnleashFeatureFetcherFactory unleashFeatureFetcherFactory = HttpFeatureFetcher::new;
437
438         private MetricSenderFactory unleashMetricSenderFactory = DefaultHttpMetricsSender::new;
439         private UnleashContextProvider contextProvider =
440                 UnleashContextProvider.getDefaultProvider();
441         private boolean synchronousFetchOnInitialisation = false;
442         private @Nullable UnleashScheduledExecutor scheduledExecutor;
443         private @Nullable UnleashSubscriber unleashSubscriber;
444         private boolean isProxyAuthenticationByJvmProperties;
445         private @Nullable Strategy fallbackStrategy;
446         private @Nullable ToggleBootstrapProvider toggleBootstrapProvider;
447         private @Nullable Proxy proxy;
448         private @Nullable Authenticator proxyAuthenticator;
449
450         private @Nullable Consumer<UnleashException> startupExceptionHandler;
451
452         private static String getHostname() {
453             String hostName = System.getProperty("hostname");
454             if (hostName == null || hostName.isEmpty()) {
455                 try {
456                     hostName = InetAddress.getLocalHost().getHostName();
457                 } catch (UnknownHostException e) {
458                 }
459             }
460             return hostName + "-";
461         }
462
463         static String getDefaultInstanceId() {
464             return getHostname() + "generated-" + Math.round(Math.random() * 1000000.0D);
465         }
466
467         static String getDefaultConnectionId() {
468             return UUID.randomUUID().toString();
469         }
470
471         public Builder unleashAPI(URI unleashAPI) {
472             this.unleashAPI = unleashAPI;
473             return this;
474         }
475
476         public Builder unleashAPI(String unleashAPI) {
477             this.unleashAPI = URI.create(unleashAPI);
478             return this;
479         }
480
481         public Builder customHttpHeader(String name, String value) {
482             this.customHttpHeaders.put(name, value);
483             return this;
484         }
485
486         public Builder customHttpHeadersProvider(CustomHttpHeadersProvider provider) {
487             this.customHttpHeadersProvider = provider;
488             return this;
489         }
490
491         public Builder appName(String appName) {
492             this.appName = appName;
493             return this;
494         }
495
496         public Builder environment(String environment) {
497             this.environment = environment;
498             return this;
499         }
500
501         public Builder instanceId(String instanceId) {
502             this.instanceId = instanceId;
503             return this;
504         }
505
506         public Builder projectName(String projectName) {
507             this.projectName = projectName;
508             return this;
509         }
510
511         public Builder namePrefix(String namePrefix) {
512             this.namePrefix = namePrefix;
513             return this;
514         }
515
516         public Builder unleashFeatureFetcherFactory(
517                 UnleashFeatureFetcherFactory unleashFeatureFetcherFactory) {
518             this.unleashFeatureFetcherFactory = unleashFeatureFetcherFactory;
519             return this;
520         }
521
522         public Builder metricsSenderFactory(MetricSenderFactory metricSenderFactory) {
523             this.unleashMetricSenderFactory = metricSenderFactory;
524             return this;
525         }
526
527         public Builder fetchTogglesInterval(long fetchTogglesInterval) {
528             this.fetchTogglesInterval = fetchTogglesInterval;
529             return this;
530         }
531
532         public Builder fetchTogglesConnectTimeout(Duration connectTimeout) {
533             this.fetchTogglesConnectTimeout = connectTimeout;
534             return this;
535         }
536
537         public Builder fetchTogglesConnectTimeoutSeconds(long connectTimeoutSeconds) {
538             this.fetchTogglesConnectTimeout = Duration.ofSeconds(connectTimeoutSeconds);
539             return this;
540         }
541
542         public Builder fetchTogglesReadTimeout(Duration readTimeout) {
543             this.fetchTogglesReadTimeout = readTimeout;
544             return this;
545         }
546
547         public Builder fetchTogglesReadTimeoutSeconds(long readTimeoutSeconds) {
548             this.fetchTogglesReadTimeout = Duration.ofSeconds(readTimeoutSeconds);
549             return this;
550         }
551
552         public Builder sendMetricsInterval(long sendMetricsInterval) {
553             this.sendMetricsInterval = sendMetricsInterval;
554             return this;
555         }
556
557         /** * Don't poll for feature toggle updates */
558         public Builder disablePolling() {
559             this.disablePolling = true;
560             return this;
561         }
562
563         public Builder sendMetricsConnectTimeout(Duration connectTimeout) {
564             this.sendMetricsConnectTimeout = connectTimeout;
565             return this;
566         }
567
568         public Builder sendMetricsConnectTimeoutSeconds(long connectTimeoutSeconds) {
569             this.sendMetricsConnectTimeout = Duration.ofSeconds(connectTimeoutSeconds);
570             return this;
571         }
572
573         public Builder sendMetricsReadTimeout(Duration readTimeout) {
574             this.sendMetricsReadTimeout = readTimeout;
575             return this;
576         }
577
578         public Builder sendMetricsReadTimeoutSeconds(long readTimeoutSeconds) {
579             this.sendMetricsReadTimeout = Duration.ofSeconds(readTimeoutSeconds);
580             return this;
581         }
582
583         /**
584          * Don't send metrics to Unleash server
585          *
586          * @return
587          */

588         public Builder disableMetrics() {
589             this.disableMetrics = true;
590             return this;
591         }
592
593         public Builder backupFile(String backupFile) {
594             this.backupFile = backupFile;
595             return this;
596         }
597
598         public Builder enableProxyAuthenticationByJvmProperties() {
599             this.isProxyAuthenticationByJvmProperties = true;
600             return this;
601         }
602
603         public Builder unleashContextProvider(UnleashContextProvider contextProvider) {
604             this.contextProvider = contextProvider;
605             return this;
606         }
607
608         public Builder synchronousFetchOnInitialisation(boolean enable) {
609             this.synchronousFetchOnInitialisation = enable;
610             return this;
611         }
612
613         public Builder scheduledExecutor(UnleashScheduledExecutor scheduledExecutor) {
614             this.scheduledExecutor = scheduledExecutor;
615             return this;
616         }
617
618         public Builder subscriber(UnleashSubscriber unleashSubscriber) {
619             this.unleashSubscriber = unleashSubscriber;
620             return this;
621         }
622
623         public Builder fallbackStrategy(@Nullable Strategy fallbackStrategy) {
624             this.fallbackStrategy = fallbackStrategy;
625             return this;
626         }
627
628         public Builder toggleBootstrapProvider(
629                 @Nullable ToggleBootstrapProvider toggleBootstrapProvider) {
630             this.toggleBootstrapProvider = toggleBootstrapProvider;
631             return this;
632         }
633
634         public Builder proxy(Proxy proxy) {
635             this.proxy = proxy;
636             return this;
637         }
638
639         public Builder proxy(
640                 Proxy proxy, @Nullable String proxyUser, @Nullable String proxyPassword) {
641             this.proxy = proxy;
642
643             if (proxyUser != null && proxyPassword != null) {
644                 this.proxyAuthenticator =
645                         new CustomProxyAuthenticator(proxy, proxyUser, proxyPassword);
646             }
647             return this;
648         }
649
650         private String getBackupFile() {
651             if (backupFile != null) {
652                 return backupFile;
653             } else {
654                 String fileName = "unleash-" + sanitizedAppName(appName) + "-repo.json";
655                 String tmpDir = System.getProperty("java.io.tmpdir");
656                 if (tmpDir == null) {
657                     throw new IllegalStateException(
658                             "'java.io.tmpdir' must not be empty, cause we write backup files into it.");
659                 }
660                 tmpDir = !tmpDir.endsWith(File.separator) ? tmpDir + File.separatorChar : tmpDir;
661                 return tmpDir + fileName;
662             }
663         }
664
665         private String sanitizedAppName(String appName) {
666             if (null == appName) {
667                 return "default";
668             } else if (appName.contains("/") || appName.contains("\\")) {
669                 return appName.replace("/""-").replace("\\""-");
670             } else {
671                 return appName;
672             }
673         }
674
675         /**
676          * Adds a custom http header for authorizing the client
677          *
678          * @param apiKey the client key to use to connect to the Unleash Server
679          * @return
680          */

681         public Builder apiKey(String apiKey) {
682             this.customHttpHeaders.put("Authorization", apiKey);
683             return this;
684         }
685
686         /**
687          * Used to handle exceptions when starting up synchronously. Allows user the option to
688          * choose how errors should be handled.
689          *
690          * @param startupExceptionHandler - a lambda taking the Exception and doing what it wants to
691          *     the system.
692          */

693         public Builder startupExceptionHandler(
694                 @Nullable Consumer<UnleashException> startupExceptionHandler) {
695             this.startupExceptionHandler = startupExceptionHandler;
696             return this;
697         }
698
699         public UnleashConfig build() {
700             return new UnleashConfig(
701                     unleashAPI,
702                     customHttpHeaders,
703                     customHttpHeadersProvider,
704                     appName,
705                     environment,
706                     instanceId,
707                     connectionId,
708                     sdkVersion,
709                     getBackupFile(),
710                     projectName,
711                     namePrefix,
712                     fetchTogglesInterval,
713                     fetchTogglesConnectTimeout,
714                     fetchTogglesReadTimeout,
715                     disablePolling,
716                     sendMetricsInterval,
717                     sendMetricsConnectTimeout,
718                     sendMetricsReadTimeout,
719                     disableMetrics,
720                     contextProvider,
721                     isProxyAuthenticationByJvmProperties,
722                     synchronousFetchOnInitialisation,
723                     unleashFeatureFetcherFactory,
724                     unleashMetricSenderFactory,
725                     Optional.ofNullable(scheduledExecutor)
726                             .orElseGet(UnleashScheduledExecutorImpl::getInstance),
727                     Optional.ofNullable(unleashSubscriber).orElseGet(NoOpSubscriber::new),
728                     fallbackStrategy,
729                     toggleBootstrapProvider,
730                     proxy,
731                     proxyAuthenticator,
732                     startupExceptionHandler);
733         }
734
735         public String getDefaultSdkVersion() {
736             String version =
737                     Optional.ofNullable(getClass().getPackage().getImplementationVersion())
738                             .orElse("development");
739             return "unleash-client-java:" + version;
740         }
741     }
742 }
743