Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions java/lance-namespace-apache-client/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -267,7 +267,7 @@
<!-- test dependencies -->
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter-api</artifactId>
<artifactId>junit-jupiter</artifactId>
<version>${junit-version}</version>
<scope>test</scope>
</dependency>
Expand All @@ -280,6 +280,6 @@
<jackson-databind-nullable-version>0.2.6</jackson-databind-nullable-version>
<jakarta-annotation-version>1.3.5</jakarta-annotation-version>
<beanvalidation-version>2.0.2</beanvalidation-version>
<junit-version>5.8.2</junit-version>
<junit-version>5.10.2</junit-version>
</properties>
</project>
Original file line number Diff line number Diff line change
Expand Up @@ -143,6 +143,8 @@ public class ApiClient extends JavaTimeFormatter {

private DateFormat dateFormat;

private HeaderProvider headerProvider;

// Methods that can have a request body
private static List<String> bodyMethods = Arrays.asList("POST", "PUT", "DELETE", "PATCH");

Expand Down Expand Up @@ -361,6 +363,38 @@ public void setBearerToken(Supplier<String> tokenSupplier) {
throw new RuntimeException("No Bearer authentication configured!");
}

/**
* Get the header provider.
*
* @return the header provider, or null if not set
*/
public HeaderProvider getHeaderProvider() {
return headerProvider;
}

/**
* Set a header provider that supplies dynamic headers for each request.
*
* <p>Use this for headers that may change over time, such as authentication tokens that need
* periodic refresh. The provider is called before each API request.
*
* <p>Example:
*
* <pre>{@code
* client.setHeaderProvider(() -> {
* String token = refreshTokenIfNeeded();
* return Map.of("Authorization", "Bearer " + token);
* });
* }</pre>
*
* @param headerProvider the header provider
* @return API client
*/
public ApiClient setHeaderProvider(HeaderProvider headerProvider) {
this.headerProvider = headerProvider;
return this;
}

/**
* Helper method to set API key value for the first API key authentication.
*
Expand Down Expand Up @@ -1071,6 +1105,16 @@ public <T> T invokeAPI(
}
}

// Add headers from header provider (if configured)
if (headerProvider != null) {
Map<String, String> providerHeaders = headerProvider.getHeaders();
if (providerHeaders != null) {
for (Map.Entry<String, String> keyValue : providerHeaders.entrySet()) {
builder.addHeader(keyValue.getKey(), keyValue.getValue());
}
}
}

BasicCookieStore store = new BasicCookieStore();
for (Entry<String, String> keyValue : cookieParams.entrySet()) {
store.addCookie(buildCookie(keyValue.getKey(), keyValue.getValue(), builder.getUri()));
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
/*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.lance.namespace.client.apache;

import java.util.Map;

/**
* Provides headers dynamically for each API request.
*
* <p>Use this interface to supply headers that may change over time, such as authentication tokens
* that need periodic refresh.
*
* <p>Example usage:
*
* <pre>{@code
* ApiClient client = new ApiClient();
* client.setHeaderProvider(() -> {
* String token = refreshTokenIfNeeded();
* Map<String, String> headers = new HashMap<>();
* headers.put("Authorization", "Bearer " + token);
* return headers;
* });
* }</pre>
*/
@FunctionalInterface
public interface HeaderProvider {
/**
* Get headers to add to the request.
*
* <p>This method is called before each API request, allowing dynamic headers to be provided.
*
* @return A map of header names to values. May return an empty map but should not return null.
*/
Map<String, String> getHeaders();
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,174 @@
/*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.lance.namespace.client.apache;

import static org.junit.jupiter.api.Assertions.*;

import java.util.Collections;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.atomic.AtomicInteger;

import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;

/** Tests for HeaderProvider functionality in ApiClient. */
public class HeaderProviderTest {

private ApiClient apiClient;

@BeforeEach
void setUp() {
apiClient = new ApiClient();
}

@Test
void testHeaderProviderDefaultsToNull() {
assertNull(apiClient.getHeaderProvider());
}

@Test
void testSetHeaderProvider() {
HeaderProvider provider = () -> Collections.singletonMap("Authorization", "Bearer test-token");
apiClient.setHeaderProvider(provider);

assertNotNull(apiClient.getHeaderProvider());
assertEquals(provider, apiClient.getHeaderProvider());
}

@Test
void testSetHeaderProviderReturnsApiClient() {
HeaderProvider provider = () -> Collections.singletonMap("Authorization", "Bearer test-token");
ApiClient result = apiClient.setHeaderProvider(provider);

assertSame(apiClient, result);
}

@Test
void testHeaderProviderReturnsHeaders() {
Map<String, String> expectedHeaders = new HashMap<>();
expectedHeaders.put("Authorization", "Bearer my-token");
expectedHeaders.put("X-Custom-Header", "custom-value");

HeaderProvider provider = () -> expectedHeaders;
apiClient.setHeaderProvider(provider);

Map<String, String> headers = apiClient.getHeaderProvider().getHeaders();
assertEquals("Bearer my-token", headers.get("Authorization"));
assertEquals("custom-value", headers.get("X-Custom-Header"));
}

@Test
void testHeaderProviderCalledEachTime() {
AtomicInteger callCount = new AtomicInteger(0);

HeaderProvider provider = () -> {
int count = callCount.incrementAndGet();
return Collections.singletonMap("Authorization", "Bearer token-" + count);
};

apiClient.setHeaderProvider(provider);

// Call getHeaders multiple times
Map<String, String> headers1 = apiClient.getHeaderProvider().getHeaders();
Map<String, String> headers2 = apiClient.getHeaderProvider().getHeaders();
Map<String, String> headers3 = apiClient.getHeaderProvider().getHeaders();

assertEquals(3, callCount.get());
assertEquals("Bearer token-1", headers1.get("Authorization"));
assertEquals("Bearer token-2", headers2.get("Authorization"));
assertEquals("Bearer token-3", headers3.get("Authorization"));
}

@Test
void testHeaderProviderEmptyMap() {
HeaderProvider provider = HashMap::new;
apiClient.setHeaderProvider(provider);

Map<String, String> headers = apiClient.getHeaderProvider().getHeaders();
assertNotNull(headers);
assertTrue(headers.isEmpty());
}

@Test
void testHeaderProviderWithLambda() {
apiClient.setHeaderProvider(() -> Collections.singletonMap("X-Api-Key", "secret-key"));

Map<String, String> headers = apiClient.getHeaderProvider().getHeaders();
assertEquals("secret-key", headers.get("X-Api-Key"));
}

@Test
void testStatefulHeaderProvider() {
// Simulate a token provider that refreshes tokens
StatefulTokenProvider tokenProvider = new StatefulTokenProvider();
apiClient.setHeaderProvider(tokenProvider);

// First call
Map<String, String> headers1 = apiClient.getHeaderProvider().getHeaders();
assertEquals("Bearer token-v1", headers1.get("Authorization"));

// Simulate token refresh
tokenProvider.refreshToken();

// Second call should have new token
Map<String, String> headers2 = apiClient.getHeaderProvider().getHeaders();
assertEquals("Bearer token-v2", headers2.get("Authorization"));
}

@Test
void testHeaderProviderCanBeCleared() {
HeaderProvider provider = () -> Collections.singletonMap("Authorization", "Bearer token");
apiClient.setHeaderProvider(provider);
assertNotNull(apiClient.getHeaderProvider());

apiClient.setHeaderProvider(null);
assertNull(apiClient.getHeaderProvider());
}

@Test
void testHeaderProviderWithMultipleHeaders() {
HeaderProvider provider = () -> {
Map<String, String> headers = new HashMap<>();
headers.put("Authorization", "Bearer token");
headers.put("X-Request-Id", "req-123");
headers.put("X-Correlation-Id", "corr-456");
headers.put("Accept-Language", "en-US");
return headers;
};

apiClient.setHeaderProvider(provider);

Map<String, String> headers = apiClient.getHeaderProvider().getHeaders();
assertEquals(4, headers.size());
assertEquals("Bearer token", headers.get("Authorization"));
assertEquals("req-123", headers.get("X-Request-Id"));
assertEquals("corr-456", headers.get("X-Correlation-Id"));
assertEquals("en-US", headers.get("Accept-Language"));
}

/** Helper class to simulate a stateful token provider. */
private static class StatefulTokenProvider implements HeaderProvider {
private int tokenVersion = 1;

@Override
public Map<String, String> getHeaders() {
return Collections.singletonMap("Authorization", "Bearer token-v" + tokenVersion);
}

public void refreshToken() {
tokenVersion++;
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -269,6 +269,13 @@ def call_api(
"""

try:
# Add headers from header_provider if configured
if self.configuration.header_provider:
provider_headers = self.configuration.header_provider()
if provider_headers:
header_params = header_params or {}
header_params.update(provider_headers)

# perform request and return response
response_data = self.rest_client.request(
method, url,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -251,6 +251,18 @@ def __init__(
self.access_token = access_token
"""Access token
"""
self.header_provider = None
"""Optional callable that returns a dict of headers to add to each request.
Use this for dynamic headers like auth tokens that need periodic refresh.
The callable should return Dict[str, str].

Example:
def get_headers():
return {"Authorization": f"Bearer {get_fresh_token()}"}

config = Configuration()
config.header_provider = get_headers
"""
self.logger = {}
"""Logging Settings
"""
Expand Down
Loading