Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
83 commits
Select commit Hold shift + click to select a range
0cf6ccf
fix: cross-SDK test compatibility fixes
joalves Jan 19, 2026
e399c35
fix: remove mutation during read lock in audienceMatches
joalves Jan 20, 2026
419c47e
feat: add comprehensive test coverage improvements
joalves Jan 27, 2026
935a36b
docs: add Spring Boot, Android examples and cancellation patterns to …
joalves Jan 30, 2026
07c067d
fix: remove blocking calls and resource leaks from Java Android examples
joalves Jan 30, 2026
12365ed
test: add canonical test parity (265 → 294 tests)
joalves Feb 6, 2026
38327cb
fix: resolve critical bugs for 100% test pass rate
joalves Feb 21, 2026
a7a3604
fix: correct IN operator arg order and null equality handling
joalves Feb 22, 2026
76e0293
feat: add convenience ABsmartly.create(endpoint, apiKey, app, env) fa…
joalves Feb 26, 2026
e4b79c0
refactor: replace positional create() factory with Builder pattern
joalves Feb 27, 2026
05a8039
docs: update README documentation
joalves Mar 9, 2026
61dd56c
fix: restore events on publish failure and allow override after close
joalves Mar 15, 2026
b9d48f6
feat(java-sdk): add readyError() method to Context
joalves Mar 15, 2026
f66822a
feat(java-sdk): add @Deprecated finalize aliases and standardize erro…
joalves Mar 15, 2026
980f8fb
fix: read methods return safe defaults when context not ready or closed
joalves Mar 15, 2026
4e1d396
feat: cross-SDK consistency fixes — all 201 scenarios passing
joalves Mar 17, 2026
ba6c3e5
refactor: rename ContextEventHandler to ContextPublisher
joalves Mar 18, 2026
2b2775c
fix: address coderabbit review issues
joalves Mar 18, 2026
c70c02d
fix: add setContextPublisher to deprecated ABSmartlyConfig
joalves Mar 18, 2026
06cefe5
fix(jsonexpr): restore haystack-first IN operator (CONTAINS semantics)
joalves Jun 15, 2026
2ee5836
fix(jsonexpr): eq(null, null) returns null (canonical null-operand ha…
joalves Jun 16, 2026
056008d
ci: build on JDK 17, apply spotless, fix example dependency
joalves Jun 16, 2026
5ca4b58
fix: UTF-8 encode astral characters correctly in unit hashing
joalves Jun 17, 2026
1abae87
test: add hermetic local-HTTP integration test for real fetch/publish
joalves Jun 25, 2026
e84b7c8
fix: restore Java 1.6 compatibility (build, CI, Buffers shim)
marcio-absmartly Aug 28, 2026
abed75c
feat: add Animal Sniffer Java 1.6 API floor enforcement
marcio-absmartly Aug 28, 2026
617bcf2
fix: restore com.absmartly.sdk.ABSmartly and ABSmartlyConfig at origi…
marcio-absmartly Aug 28, 2026
e8c2987
fix: restore deprecated Context.create(ContextEventHandler) overload …
marcio-absmartly Aug 28, 2026
63e094d
fix: remove Context.finalize() to prevent GC-triggered blocking publish
marcio-absmartly Aug 28, 2026
0543a47
fix: replace CallerRunsPolicy with AbortPolicy in REGEX_POOL; cancel …
marcio-absmartly Aug 28, 2026
f0475e7
fix: correct publish-accounting in Context.flush() (addAndGet + close…
marcio-absmartly Aug 28, 2026
1e15a6c
fix: comment and test hygiene (HashingTest, ContextFixTest, Concurren…
marcio-absmartly Aug 28, 2026
6552512
fix: size REGEX_POOL from available processors with bounded queue and…
marcio-absmartly Aug 28, 2026
59f7699
test: add deterministic rejection test for MatchOperator pool saturation
marcio-absmartly Aug 28, 2026
fc17886
test: add deterministic publish-accounting regression tests
marcio-absmartly Aug 28, 2026
1fff931
test: cover facade classes; remove ABSmartly/ABSmartlyConfig jacoco e…
marcio-absmartly Aug 28, 2026
77387dc
fix: comment and naming cleanups
marcio-absmartly Aug 28, 2026
84150e0
test: simplify review regression coverage
marcio-absmartly Aug 28, 2026
5c38dc9
test: bound publish-accounting test waits with @Timeout
marcio-absmartly Aug 28, 2026
4dbfa29
docs: close context with try-with-resources in server examples
marcio-absmartly Aug 29, 2026
7e9b0e0
test: name the http-endpoint test for what it asserts
marcio-absmartly Aug 29, 2026
c8b29b6
test: exercise DefaultHTTPClientRetryStrategy directly
marcio-absmartly Aug 29, 2026
a0603e3
test: restore the original retry-strategy tests
marcio-absmartly Aug 29, 2026
de9fffd
fix: await in-flight publish before closing a context
marcio-absmartly Aug 31, 2026
135487c
fix: isolate the Publish log event from publish delivery accounting
marcio-absmartly Aug 31, 2026
e6c3714
fix: leave injected schedulers running when ABsmartly closes
marcio-absmartly Aug 31, 2026
7032988
fix: synchronize close with publish restoration
marcio-absmartly Aug 31, 2026
b17b7d3
fix: await overlapping context publishes
marcio-absmartly Aug 31, 2026
65fda17
fix: restore events when publisher throws
marcio-absmartly Aug 31, 2026
faeb67b
test: bound scheduler close checks
marcio-absmartly Aug 31, 2026
0f0e910
style: format publish future array
marcio-absmartly Aug 31, 2026
694df52
docs: record close sequencing verification
marcio-absmartly Aug 31, 2026
07be5b9
fix: settle publishes before deregistration
marcio-absmartly Aug 31, 2026
2c9d630
fix: isolate error logger from publish settlement
marcio-absmartly Aug 31, 2026
8dc1fc7
test: remove nondeterministic close regression
marcio-absmartly Aug 31, 2026
0916adb
test: cover publish settlement ordering
marcio-absmartly Aug 31, 2026
b99c900
test: simplify close settlement retry
marcio-absmartly Aug 31, 2026
3b046cb
style: simplify closing publish array copy
marcio-absmartly Aug 31, 2026
71510f9
fix: isolate publish lifecycle from callers
marcio-absmartly Aug 31, 2026
84edd10
fix: allow publishers to close context
marcio-absmartly Aug 31, 2026
e1a5ec3
fix: narrow publisher invocation tracking
marcio-absmartly Aug 31, 2026
5c94b65
fix: reopen context after publish restoration
marcio-absmartly Aug 31, 2026
457497d
fix: recheck pending events before close
marcio-absmartly Aug 31, 2026
0c30999
fix: reject close from publisher callback
marcio-absmartly Aug 31, 2026
0609b6f
test: enforce publisher close contract
marcio-absmartly Aug 31, 2026
fe1daed
Revert "test: enforce publisher close contract"
marcio-absmartly Aug 31, 2026
41e4325
Revert "fix: reject close from publisher callback"
marcio-absmartly Aug 31, 2026
0bab8e5
Revert "fix: recheck pending events before close"
marcio-absmartly Aug 31, 2026
14453b7
Revert "fix: reopen context after publish restoration"
marcio-absmartly Aug 31, 2026
436ba37
Revert "fix: allow publishers to close context"
marcio-absmartly Aug 31, 2026
ba60895
fix: allow publishers to close context
marcio-absmartly Aug 31, 2026
056e3bc
test: cover publishers closing context
marcio-absmartly Aug 31, 2026
78802a6
test: bound publisher close waits
marcio-absmartly Aug 31, 2026
bcaee35
fix: restore ABSmartly API names
marcio-absmartly Sep 1, 2026
2fdabf6
test: restore ABSmartly test names
marcio-absmartly Sep 1, 2026
cf22679
docs: restore ABSmartly examples
marcio-absmartly Sep 1, 2026
40b360b
fix: remove regex execution limits
marcio-absmartly Sep 1, 2026
0f89ea6
test: cover unrestricted regex operands
marcio-absmartly Sep 1, 2026
d8c2e79
fix: restore allocation-free UTF-8 encoding
marcio-absmartly Sep 1, 2026
843a00d
fix: restore ContextEventHandler API names
marcio-absmartly Sep 1, 2026
bd22bb3
test: restore ContextEventHandler type names
marcio-absmartly Sep 1, 2026
a895178
test: format restored handler matcher
marcio-absmartly Sep 1, 2026
39997ff
fix: preserve unpaired surrogate hashes
marcio-absmartly Sep 1, 2026
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 .github/workflows/main.yml
Original file line number Diff line number Diff line change
Expand Up @@ -12,11 +12,11 @@ jobs:
steps:
- uses: actions/checkout@v3

- name: Setup Java 8
- name: Setup Java 11
uses: actions/setup-java@v3
with:
distribution: "adopt"
java-version: "8"
java-version: "11"
cache: "gradle"

- name: Build Java SDK
Expand Down
4 changes: 2 additions & 2 deletions .github/workflows/publish.yml
Original file line number Diff line number Diff line change
Expand Up @@ -11,11 +11,11 @@ jobs:
steps:
- uses: actions/checkout@v3

- name: Setup Java 8
- name: Setup Java 11
uses: actions/setup-java@v3
with:
distribution: "adopt"
java-version: "8"
java-version: "11"
cache: "gradle"

- name: Publish JavaSDK
Expand Down
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -11,3 +11,4 @@
*.log
build/
out/
.claude/
712 changes: 574 additions & 138 deletions README.md

Large diffs are not rendered by default.

18 changes: 8 additions & 10 deletions build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,12 @@ plugins {
id "java"
id "groovy"
id "jacoco"
id "findbugs"

id "com.diffplug.spotless" version "5.8.2"
id "com.adarshr.test-logger" version "2.1.1"
id "org.barfuin.gradle.jacocolog" version "1.2.3"
id "io.github.gradle-nexus.publish-plugin" version "1.1.0"
id "org.owasp.dependencycheck" version "7.3.0"
id "com.diffplug.spotless" version "6.25.0"
id "com.adarshr.test-logger" version "4.0.0"
id "org.barfuin.gradle.jacocolog" version "3.1.0"
id "io.github.gradle-nexus.publish-plugin" version "1.3.0"
id "ru.vyarus.animalsniffer" version "1.7.1" apply false
}


Expand All @@ -21,22 +20,21 @@ ext {
jacksonVersion = "2.13.4.2"
jacksonDataTypeVersion = "2.13.4"

junitVersion = "5.7.0"
mockitoVersion = "3.6.28"
junitVersion = "5.10.2"
mockitoVersion = "5.11.0"
}


allprojects {
group = GROUP_ID

apply plugin: "java"
apply plugin: "org.owasp.dependencycheck"
apply from: rootProject.file("gradle/repositories.gradle")
apply from: rootProject.file("gradle/spotless.gradle")
apply from: rootProject.file("gradle/findbugs.gradle")
apply from: rootProject.file("gradle/test-logger.gradle")
apply from: rootProject.file("gradle/jacoco.gradle")
apply from: rootProject.file("gradle/coverage-logger.gradle")
apply from: rootProject.file("gradle/compatibility.gradle")

compileJava {
sourceCompatibility = "1.6"
Expand Down
19 changes: 12 additions & 7 deletions core-api/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -26,18 +26,21 @@ dependencies {
testImplementation group: "org.junit.jupiter", name: "junit-jupiter-params", version: junitVersion
testRuntimeOnly group: "org.junit.jupiter", name: "junit-jupiter-engine", version: junitVersion
testImplementation group: "org.mockito", name: "mockito-core", version: mockitoVersion
testImplementation group: "org.mockito", name: "mockito-inline", version: mockitoVersion
testImplementation group: "org.mockito", name: "mockito-junit-jupiter", version: mockitoVersion
}

check.dependsOn jacocoTestCoverageVerification

def jacocoExcludes = [
"com/absmartly/sdk/json/**/*",
"com/absmartly/sdk/deprecated/**/*",
"com/absmartly/sdk/java/**/*",
]

jacocoTestReport {
afterEvaluate {
getClassDirectories().setFrom(classDirectories.files.collect {
fileTree(dir: it, exclude: [
"com/absmartly/core-api/json/**/*"
])
fileTree(dir: it, exclude: jacocoExcludes)
})
}
}
Expand Down Expand Up @@ -67,16 +70,18 @@ jacocoTestCoverageVerification {

afterEvaluate {
getClassDirectories().setFrom(classDirectories.files.collect {
fileTree(dir: it, exclude: [
"com/absmartly/core-api/json/**/*"
])
fileTree(dir: it, exclude: jacocoExcludes)
})
}
}


test {
useJUnitPlatform()
jvmArgs '--add-opens', 'java.base/java.lang=ALL-UNNAMED',
'--add-opens', 'java.base/java.lang.reflect=ALL-UNNAMED',
'--add-opens', 'java.base/java.util=ALL-UNNAMED',
'--add-opens', 'java.base/java.util.concurrent=ALL-UNNAMED'
}

publishToSonatype.dependsOn check
105 changes: 97 additions & 8 deletions core-api/src/main/java/com/absmartly/sdk/ABSmartly.java
Original file line number Diff line number Diff line change
Expand Up @@ -17,13 +17,79 @@ public static ABSmartly create(@Nonnull ABSmartlyConfig config) {
return new ABSmartly(config);
}

public static Builder builder() {
return new Builder();
}

public static class Builder {
private String endpoint;
private String apiKey;
private String application;
private String environment;
private ContextEventLogger eventLogger;

Builder() {}

public Builder endpoint(@Nonnull String endpoint) {
this.endpoint = endpoint;
return this;
}

public Builder apiKey(@Nonnull String apiKey) {
this.apiKey = apiKey;
return this;
}

public Builder application(@Nonnull String application) {
this.application = application;
return this;
}

public Builder environment(@Nonnull String environment) {
this.environment = environment;
return this;
}

public Builder eventLogger(@Nonnull ContextEventLogger eventLogger) {
this.eventLogger = eventLogger;
return this;
}

public ABSmartly build() {
if (endpoint == null)
throw new IllegalArgumentException("endpoint is required");
if (apiKey == null)
throw new IllegalArgumentException("apiKey is required");
if (application == null)
throw new IllegalArgumentException("application is required");
if (environment == null)
throw new IllegalArgumentException("environment is required");

final ClientConfig clientConfig = ClientConfig.create()
.setEndpoint(endpoint)
.setAPIKey(apiKey)
.setApplication(application)
.setEnvironment(environment);

final ABSmartlyConfig config = ABSmartlyConfig.create()
.setClient(Client.create(clientConfig));

if (eventLogger != null) {
config.setContextEventLogger(eventLogger);
}

return create(config);
}
}

private ABSmartly(@Nonnull ABSmartlyConfig config) {
contextDataProvider_ = config.getContextDataProvider();
contextEventHandler_ = config.getContextEventHandler();
contextEventLogger_ = config.getContextEventLogger();
variableParser_ = config.getVariableParser();
audienceDeserializer_ = config.getAudienceDeserializer();
scheduler_ = config.getScheduler();
ownsScheduler_ = scheduler_ == null;

if ((contextDataProvider_ == null) || (contextEventHandler_ == null)) {
client_ = config.getClient();
Expand Down Expand Up @@ -54,36 +120,58 @@ private ABSmartly(@Nonnull ABSmartlyConfig config) {
}

public Context createContext(@Nonnull ContextConfig config) {
checkNotClosed();
return Context.create(Clock.systemUTC(), config, scheduler_, contextDataProvider_.getContextData(),
contextDataProvider_, contextEventHandler_, contextEventLogger_, variableParser_,
new AudienceMatcher(audienceDeserializer_));
}

public Context createContextWith(@Nonnull ContextConfig config, ContextData data) {
checkNotClosed();
return Context.create(Clock.systemUTC(), config, scheduler_, CompletableFuture.completedFuture(data),
contextDataProvider_, contextEventHandler_, contextEventLogger_, variableParser_,
new AudienceMatcher(audienceDeserializer_));
}

public CompletableFuture<ContextData> getContextData() {
checkNotClosed();
return contextDataProvider_.getContextData();
}

private void checkNotClosed() {
if (closed_) {
throw new IllegalStateException("ABSmartly instance is closed");
}
}

@Override
public void close() throws IOException {
if (client_ != null) {
client_.close();
client_ = null;
if (closed_) {
return;
}
closed_ = true;

if (scheduler_ != null) {
try {
scheduler_.awaitTermination(5000, TimeUnit.MILLISECONDS);
} catch (InterruptedException ignored) {}
scheduler_ = null;
try {
if (client_ != null) {
client_.close();
}
} finally {
// A caller-supplied scheduler remains under caller ownership and must be left running.
if ((scheduler_ != null) && ownsScheduler_) {
scheduler_.shutdown();
try {
if (!scheduler_.awaitTermination(5000, TimeUnit.MILLISECONDS)) {
scheduler_.shutdownNow();
}
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
scheduler_.shutdownNow();
}
}
}
}

private volatile boolean closed_;
private Client client_;
private ContextDataProvider contextDataProvider_;
private ContextEventHandler contextEventHandler_;
Expand All @@ -92,4 +180,5 @@ public void close() throws IOException {

private AudienceDeserializer audienceDeserializer_;
private ScheduledExecutorService scheduler_;
private final boolean ownsScheduler_;
}
3 changes: 3 additions & 0 deletions core-api/src/main/java/com/absmartly/sdk/AudienceMatcher.java
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,9 @@ public boolean get() {
}

public Result evaluate(String audience, Map<String, Object> attributes) {
if (audience == null || audience.isEmpty()) {
return null;
}
final byte[] bytes = audience.getBytes(StandardCharsets.UTF_8);
final Map<String, Object> audienceMap = deserializer_.deserialize(bytes, 0, bytes.length);
if (audienceMap != null) {
Expand Down
33 changes: 30 additions & 3 deletions core-api/src/main/java/com/absmartly/sdk/Client.java
Original file line number Diff line number Diff line change
Expand Up @@ -13,10 +13,15 @@

import javax.annotation.Nonnull;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import com.absmartly.sdk.json.ContextData;
import com.absmartly.sdk.json.PublishEvent;

public class Client implements Closeable {
private static final Logger log = LoggerFactory.getLogger(Client.class);

static public Client create(@Nonnull final ClientConfig config) {
return new Client(config, DefaultHTTPClient.create(DefaultHTTPClientConfig.create()));
}
Expand All @@ -31,6 +36,15 @@ static public Client create(@Nonnull final ClientConfig config, @Nonnull final H
throw new IllegalArgumentException("Missing Endpoint configuration");
}

if (!endpoint.startsWith("https://")) {
if (endpoint.startsWith("http://")) {
log.warn("ABSmartly SDK endpoint is not using HTTPS. API keys will be transmitted in plaintext: {}",
endpoint);
} else {
throw new IllegalArgumentException("Endpoint must use http:// or https:// protocol: " + endpoint);
}
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.

final String apiKey = config.getAPIKey();
if ((apiKey == null) || apiKey.isEmpty()) {
throw new IllegalArgumentException("Missing APIKey configuration");
Expand All @@ -46,7 +60,9 @@ static public Client create(@Nonnull final ClientConfig config, @Nonnull final H
throw new IllegalArgumentException("Missing Environment configuration");
}

url_ = endpoint + "/context";
final String normalizedEndpoint = endpoint.endsWith("/") ? endpoint.substring(0, endpoint.length() - 1)
: endpoint;
url_ = normalizedEndpoint + "/context";
httpClient_ = httpClient;
deserializer_ = config.getContextDataDeserializer();
serializer_ = config.getContextEventSerializer();
Expand Down Expand Up @@ -86,8 +102,19 @@ public void accept(HTTPClient.Response response) {
final int code = response.getStatusCode();
if ((code / 100) == 2) {
final byte[] content = response.getContent();
dataFuture.complete(
deserializer_.deserialize(response.getContent(), 0, content.length));
if (content == null || content.length == 0) {
dataFuture.completeExceptionally(new IllegalStateException(
"Empty response body from context data endpoint"));
} else {
final ContextData result = deserializer_.deserialize(content, 0,
content.length);
if (result != null) {
dataFuture.complete(result);
} else {
dataFuture.completeExceptionally(new IllegalStateException(
"Failed to deserialize context data response"));
}
}
} else {
dataFuture.completeExceptionally(new Exception(response.getStatusMessage()));
}
Expand Down
Loading
Loading