|
| 1 | +package com.glean.api_client.glean_api_client.hooks; |
| 2 | + |
| 3 | +import com.glean.api_client.glean_api_client.Glean; |
| 4 | +import com.glean.api_client.glean_api_client.SDKConfiguration; |
| 5 | +import com.glean.api_client.glean_api_client.SecuritySource; |
| 6 | +import com.glean.api_client.glean_api_client.utils.HTTPClient; |
| 7 | +import com.glean.api_client.glean_api_client.utils.RetryConfig; |
| 8 | + |
| 9 | +import java.lang.reflect.Field; |
| 10 | +import java.util.Map; |
| 11 | +import java.util.Optional; |
| 12 | + |
| 13 | +/** |
| 14 | + * Builder wrapper for creating {@link Glean} instances with custom configuration options. |
| 15 | + * |
| 16 | + * <p>This builder extends the standard SDK builder with additional configuration options |
| 17 | + * for experimental features and deprecation testing that are preserved across SDK regenerations. |
| 18 | + * |
| 19 | + * <p>Example usage: |
| 20 | + * <pre>{@code |
| 21 | + * Glean glean = GleanBuilder.create() |
| 22 | + * .apiToken("your-api-token") |
| 23 | + * .instance("instance-name") |
| 24 | + * .excludeDeprecatedAfter("2026-10-15") |
| 25 | + * .includeExperimental(true) |
| 26 | + * .build(); |
| 27 | + * }</pre> |
| 28 | + */ |
| 29 | +public final class GleanBuilder { |
| 30 | + |
| 31 | + private final Glean.Builder delegate; |
| 32 | + |
| 33 | + private Optional<String> excludeDeprecatedAfter = Optional.empty(); |
| 34 | + private Optional<Boolean> includeExperimental = Optional.empty(); |
| 35 | + |
| 36 | + private GleanBuilder() { |
| 37 | + this.delegate = Glean.builder(); |
| 38 | + } |
| 39 | + |
| 40 | + /** |
| 41 | + * Creates a new builder instance. |
| 42 | + * |
| 43 | + * @return a new GleanBuilder |
| 44 | + */ |
| 45 | + public static GleanBuilder create() { |
| 46 | + return new GleanBuilder(); |
| 47 | + } |
| 48 | + |
| 49 | + /** |
| 50 | + * Configures the SDK security to use the provided API token. |
| 51 | + * |
| 52 | + * @param apiToken The API token to use for all requests. |
| 53 | + * @return This builder instance. |
| 54 | + */ |
| 55 | + public GleanBuilder apiToken(String apiToken) { |
| 56 | + delegate.apiToken(apiToken); |
| 57 | + return this; |
| 58 | + } |
| 59 | + |
| 60 | + /** |
| 61 | + * Configures the SDK to use a custom security source. |
| 62 | + * |
| 63 | + * @param securitySource The security source to use for all requests. |
| 64 | + * @return This builder instance. |
| 65 | + */ |
| 66 | + public GleanBuilder securitySource(SecuritySource securitySource) { |
| 67 | + delegate.securitySource(securitySource); |
| 68 | + return this; |
| 69 | + } |
| 70 | + |
| 71 | + /** |
| 72 | + * Allows the default HTTP client to be overridden with a custom implementation. |
| 73 | + * |
| 74 | + * @param client The HTTP client to use for all requests. |
| 75 | + * @return This builder instance. |
| 76 | + */ |
| 77 | + public GleanBuilder client(HTTPClient client) { |
| 78 | + delegate.client(client); |
| 79 | + return this; |
| 80 | + } |
| 81 | + |
| 82 | + /** |
| 83 | + * Overrides the default server URL. |
| 84 | + * |
| 85 | + * @param serverUrl The server URL to use for all requests. |
| 86 | + * @return This builder instance. |
| 87 | + */ |
| 88 | + public GleanBuilder serverURL(String serverUrl) { |
| 89 | + delegate.serverURL(serverUrl); |
| 90 | + return this; |
| 91 | + } |
| 92 | + |
| 93 | + /** |
| 94 | + * Overrides the default server URL with a templated URL populated with the provided parameters. |
| 95 | + * |
| 96 | + * @param serverUrl The server URL to use for all requests. |
| 97 | + * @param params The parameters to use when templating the URL. |
| 98 | + * @return This builder instance. |
| 99 | + */ |
| 100 | + public GleanBuilder serverURL(String serverUrl, Map<String, String> params) { |
| 101 | + delegate.serverURL(serverUrl, params); |
| 102 | + return this; |
| 103 | + } |
| 104 | + |
| 105 | + /** |
| 106 | + * Overrides the default server by index. |
| 107 | + * |
| 108 | + * @param serverIdx The server to use for all requests. |
| 109 | + * @return This builder instance. |
| 110 | + */ |
| 111 | + public GleanBuilder serverIndex(int serverIdx) { |
| 112 | + delegate.serverIndex(serverIdx); |
| 113 | + return this; |
| 114 | + } |
| 115 | + |
| 116 | + /** |
| 117 | + * Sets the instance variable for URL substitution. |
| 118 | + * |
| 119 | + * @param instance The instance name to set. |
| 120 | + * @return This builder instance. |
| 121 | + */ |
| 122 | + public GleanBuilder instance(String instance) { |
| 123 | + delegate.instance(instance); |
| 124 | + return this; |
| 125 | + } |
| 126 | + |
| 127 | + /** |
| 128 | + * Overrides the default configuration for retries. |
| 129 | + * |
| 130 | + * @param retryConfig The retry configuration to use for all requests. |
| 131 | + * @return This builder instance. |
| 132 | + */ |
| 133 | + public GleanBuilder retryConfig(RetryConfig retryConfig) { |
| 134 | + delegate.retryConfig(retryConfig); |
| 135 | + return this; |
| 136 | + } |
| 137 | + |
| 138 | + /** |
| 139 | + * Enables debug logging for HTTP requests and responses, including JSON body content. |
| 140 | + * |
| 141 | + * @param enabled Whether to enable debug logging. |
| 142 | + * @return This builder instance. |
| 143 | + */ |
| 144 | + public GleanBuilder enableHTTPDebugLogging(boolean enabled) { |
| 145 | + delegate.enableHTTPDebugLogging(enabled); |
| 146 | + return this; |
| 147 | + } |
| 148 | + |
| 149 | + /** |
| 150 | + * Exclude API endpoints that will be deprecated after this date. |
| 151 | + * Use this to test your integration against upcoming deprecations. |
| 152 | + * |
| 153 | + * <p>More information: <a href="https://developers.glean.com/deprecations/overview">Deprecations Overview</a> |
| 154 | + * |
| 155 | + * @param excludeDeprecatedAfter date string in YYYY-MM-DD format (e.g., '2026-10-15') |
| 156 | + * @return This builder instance. |
| 157 | + */ |
| 158 | + public GleanBuilder excludeDeprecatedAfter(String excludeDeprecatedAfter) { |
| 159 | + this.excludeDeprecatedAfter = Optional.ofNullable(excludeDeprecatedAfter); |
| 160 | + return this; |
| 161 | + } |
| 162 | + |
| 163 | + /** |
| 164 | + * Enable experimental API features that are not yet generally available. |
| 165 | + * Use this to preview and test new functionality. |
| 166 | + * |
| 167 | + * <p><strong>Warning:</strong> Experimental features may change or be removed without notice. |
| 168 | + * Do not rely on experimental features in production environments. |
| 169 | + * |
| 170 | + * @param includeExperimental whether to include experimental features |
| 171 | + * @return This builder instance. |
| 172 | + */ |
| 173 | + public GleanBuilder includeExperimental(boolean includeExperimental) { |
| 174 | + this.includeExperimental = Optional.of(includeExperimental); |
| 175 | + return this; |
| 176 | + } |
| 177 | + |
| 178 | + /** |
| 179 | + * Builds a new instance of the Glean SDK. |
| 180 | + * |
| 181 | + * @return The configured Glean instance. |
| 182 | + */ |
| 183 | + public Glean build() { |
| 184 | + Glean sdk = delegate.build(); |
| 185 | + SDKConfiguration sdkConfiguration = extractSdkConfiguration(sdk); |
| 186 | + if (sdkConfiguration != null) { |
| 187 | + GleanCustomConfigRegistry.put( |
| 188 | + sdkConfiguration, |
| 189 | + new GleanCustomConfig(excludeDeprecatedAfter, includeExperimental) |
| 190 | + ); |
| 191 | + } |
| 192 | + return sdk; |
| 193 | + } |
| 194 | + |
| 195 | + private static SDKConfiguration extractSdkConfiguration(Glean sdk) { |
| 196 | + if (sdk == null) { |
| 197 | + return null; |
| 198 | + } |
| 199 | + |
| 200 | + try { |
| 201 | + // Preferred: reflectively access Glean.client -> Client.sdkConfiguration |
| 202 | + Object client = readFieldValue(sdk, "client"); |
| 203 | + if (client != null) { |
| 204 | + Object cfg = readFieldValue(client, "sdkConfiguration"); |
| 205 | + if (cfg instanceof SDKConfiguration) { |
| 206 | + return (SDKConfiguration) cfg; |
| 207 | + } |
| 208 | + } |
| 209 | + } catch (RuntimeException e) { |
| 210 | + // Best-effort: fall through to generic field scan |
| 211 | + } |
| 212 | + |
| 213 | + // Fallback: scan first-level fields for an SDKConfiguration |
| 214 | + for (Field f : sdk.getClass().getDeclaredFields()) { |
| 215 | + if (!SDKConfiguration.class.isAssignableFrom(f.getType())) { |
| 216 | + continue; |
| 217 | + } |
| 218 | + try { |
| 219 | + f.setAccessible(true); |
| 220 | + Object cfg = f.get(sdk); |
| 221 | + if (cfg instanceof SDKConfiguration) { |
| 222 | + return (SDKConfiguration) cfg; |
| 223 | + } |
| 224 | + } catch (IllegalAccessException e) { |
| 225 | + // ignore |
| 226 | + } |
| 227 | + } |
| 228 | + |
| 229 | + return null; |
| 230 | + } |
| 231 | + |
| 232 | + private static Object readFieldValue(Object target, String fieldName) { |
| 233 | + Class<?> c = target.getClass(); |
| 234 | + while (c != null) { |
| 235 | + try { |
| 236 | + Field f = c.getDeclaredField(fieldName); |
| 237 | + f.setAccessible(true); |
| 238 | + return f.get(target); |
| 239 | + } catch (NoSuchFieldException e) { |
| 240 | + c = c.getSuperclass(); |
| 241 | + } catch (IllegalAccessException e) { |
| 242 | + throw new RuntimeException(e); |
| 243 | + } |
| 244 | + } |
| 245 | + return null; |
| 246 | + } |
| 247 | +} |
0 commit comments