Skip to content
Merged
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
5 changes: 5 additions & 0 deletions src/main/java/com/stripe/StripeClient.java
Original file line number Diff line number Diff line change
Expand Up @@ -1223,6 +1223,11 @@ public StripeClientBuilder setStripeContext(String context) {
return this;
}

public StripeClientBuilder setStripeContext(StripeContext context) {
this.stripeContext = context == null ? null : context.toString();
return this;
}

public String getStripeContext() {
return this.stripeContext;
}
Expand Down
98 changes: 98 additions & 0 deletions src/main/java/com/stripe/StripeContext.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
package com.stripe;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import lombok.EqualsAndHashCode;

/**
* The StripeContext class provides an immutable container for interacting with the `Stripe-Context`
* header.
*
* <p>You can use it whenever you're initializing a `StripeClient` or sending `stripe_context` with
* a request. It's also found in the `EventNotification.context` property.
*/
@EqualsAndHashCode
public final class StripeContext {
private final List<String> segments;

/** Creates a new StripeContext with no segments. */
public StripeContext() {
this(null);
}

/**
* Creates a new StripeContext with the specified segments.
*
* @param segments the list of context segments
*/
public StripeContext(List<String> segments) {
this.segments =
segments == null
? Collections.emptyList()
: Collections.unmodifiableList(new ArrayList<>(segments));
}

/**
* Returns a new StripeContext with the given segment added to the end.
*
* @param segment the segment to add
* @return a new StripeContext instance with the segment appended
*/
public StripeContext push(String segment) {
List<String> newSegments = new ArrayList<>(this.segments);
newSegments.add(segment);
return new StripeContext(newSegments);
}

/**
* Returns a new StripeContext with the last segment removed.
*
* @return a new StripeContext instance with the last segment removed
*/
public StripeContext pop() {
if (segments.isEmpty()) {
throw new IllegalStateException("Cannot pop from an empty StripeContext");
}

List<String> newSegments = new ArrayList<>(this.segments);
newSegments.remove(newSegments.size() - 1);
return new StripeContext(newSegments);
}

/**
* Converts the context to a string by joining segments with '/'.
*
* @return string representation of the context segments joined by '/', `null` if there are no
* segments (useful for clearing context)
*/
@Override
public String toString() {
return String.join("/", segments);
}

/**
* Parse a context string into a StripeContext instance.
*
* @param contextStr string to parse (segments separated by '/')
* @return StripeContext instance with segments from the string
*/
public static StripeContext parse(String contextStr) {
if (contextStr == null || contextStr.isEmpty()) {
return new StripeContext();
}

List<String> segments = Arrays.asList(contextStr.split("/"));
return new StripeContext(segments);
}

/**
* Returns an unmodifiable list of the current segments.
*
* @return the list of segments
*/
public List<String> getSegments() {
return segments;
}
}
26 changes: 26 additions & 0 deletions src/main/java/com/stripe/model/StripeContextDeserializer.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
package com.stripe.model;

import com.google.gson.JsonDeserializationContext;
import com.google.gson.JsonDeserializer;
import com.google.gson.JsonElement;
import com.google.gson.JsonParseException;
import com.stripe.StripeContext;
import java.lang.reflect.Type;

public class StripeContextDeserializer implements JsonDeserializer<StripeContext> {

@Override
public StripeContext deserialize(
JsonElement json, Type typeOfT, JsonDeserializationContext context)
throws JsonParseException {
if (json == null || json.isJsonNull()) {
return null;
}

String contextString = json.getAsString().trim();
if (contextString.isEmpty()) {
return null;
}
return StripeContext.parse(contextString);
}
}
8 changes: 6 additions & 2 deletions src/main/java/com/stripe/model/v2/EventNotification.java
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import com.google.gson.JsonObject;
import com.google.gson.annotations.SerializedName;
import com.stripe.StripeClient;
import com.stripe.StripeContext;
import com.stripe.exception.StripeException;
import com.stripe.model.StripeObject;
import com.stripe.model.v2.Event.RelatedObject;
Expand Down Expand Up @@ -71,7 +72,7 @@ public static class Reason {

/** [Optional] Authentication context needed to fetch the event or related object. */
@SerializedName("context")
public String context;
public StripeContext context;

/** [Optional] Reason for the event. */
@SerializedName("reason")
Expand All @@ -98,14 +99,17 @@ public static EventNotification fromJson(String payload, StripeClient client) {

EventNotification e = ApiResource.GSON.fromJson(payload, cls);
e.client = client;

return e;
}

private RawRequestOptions getRequestOptions() {
if (context == null) {
return null;
}
return new RawRequestOptions.RawRequestOptionsBuilder().setStripeContext(context).build();
return new RawRequestOptions.RawRequestOptionsBuilder()
.setStripeContext(context.toString())
.build();
}

/* retrieves the full payload for an event. Protected because individual push classes use it, but type it correctly */
Expand Down
2 changes: 2 additions & 0 deletions src/main/java/com/stripe/net/ApiResource.java
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package com.stripe.net;

import com.google.gson.*;
import com.stripe.StripeContext;
import com.stripe.exception.InvalidRequestException;
import com.stripe.model.*;
import com.stripe.model.v2.EventTypeAdapterFactory;
Expand Down Expand Up @@ -54,6 +55,7 @@ private static Gson createGson(boolean shouldSetResponseGetter) {
.registerTypeAdapter(EphemeralKey.class, new EphemeralKeyDeserializer())
.registerTypeAdapter(Event.Data.class, new EventDataDeserializer())
.registerTypeAdapter(Event.Request.class, new EventRequestDeserializer())
.registerTypeAdapter(StripeContext.class, new StripeContextDeserializer())
.registerTypeAdapter(ExpandableField.class, new ExpandableFieldDeserializer())
.registerTypeAdapter(Instant.class, new InstantDeserializer())
.registerTypeAdapterFactory(new EventTypeAdapterFactory())
Expand Down
7 changes: 7 additions & 0 deletions src/main/java/com/stripe/net/RawRequestOptions.java
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package com.stripe.net;

import com.stripe.StripeContext;
import java.net.PasswordAuthentication;
import java.net.Proxy;
import java.util.Map;
Expand Down Expand Up @@ -81,6 +82,12 @@ public RawRequestOptionsBuilder setStripeContext(String stripeContext) {
return this;
}

@Override
public RawRequestOptionsBuilder setStripeContext(StripeContext stripeContext) {
super.setStripeContext(stripeContext);
return this;
}

@Override
public RawRequestOptionsBuilder setStripeAccount(String stripeAccount) {
super.setStripeAccount(stripeAccount);
Expand Down
30 changes: 27 additions & 3 deletions src/main/java/com/stripe/net/RequestOptions.java
Original file line number Diff line number Diff line change
Expand Up @@ -235,6 +235,17 @@ public RequestOptionsBuilder setStripeContext(String context) {
return this;
}

public RequestOptionsBuilder setStripeContext(com.stripe.StripeContext context) {
this.stripeContext = context != null ? context.toString() : null;
return this;
}

/**
* Empties the current builder value for StripeContext, which will defer to the client options.
*
* <p>To send no context at all, call `setContext(new StripeContext())`or set the context to an
* empty string.
*/
public RequestOptionsBuilder clearStripeContext() {
this.stripeContext = null;
return this;
Expand Down Expand Up @@ -472,15 +483,28 @@ static RequestOptions merge(StripeResponseGetterOptions clientOptions, RequestOp
clientOptions.getProxyCredential() // proxyCredential
);
}

// callers need to be able to explicitly unset context per-request
// an empty StripeContext serializes to a "", so check for that and empty context out if it's
// there.
String stripeContext;
if (options.getStripeContext() != null) {
String requestContext = options.getStripeContext().trim();
if (requestContext.isEmpty()) {
stripeContext = null;
} else {
stripeContext = requestContext;
}
} else {
stripeContext = clientOptions.getStripeContext();
}
return new RequestOptions(
options.getAuthenticator() != null
? options.getAuthenticator()
: clientOptions.getAuthenticator(),
options.getClientId() != null ? options.getClientId() : clientOptions.getClientId(),
options.getIdempotencyKey(),
options.getStripeContext() != null
? options.getStripeContext()
: clientOptions.getStripeContext(),
stripeContext,
options.getStripeAccount() != null
? options.getStripeAccount()
: clientOptions.getStripeAccount(),
Expand Down
2 changes: 1 addition & 1 deletion src/test/java/com/stripe/StripeClientTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -248,7 +248,7 @@ public void parsesEventNotificationWithRelatedObject()
assertEquals("evt_234", eventNotification.getId());
assertEquals("v1.billing.meter.error_report_triggered", eventNotification.getType());
assertEquals(Instant.parse("2022-02-15T00:27:45.330Z"), eventNotification.created);
assertEquals("org_123", eventNotification.context);
assertEquals("org_123", eventNotification.getContext().toString());
assertInstanceOf(V1BillingMeterErrorReportTriggeredEventNotification.class, eventNotification);
assertEquals("request", eventNotification.getReason().getType());
assertEquals("abc123", eventNotification.getReason().getRequest().getId());
Expand Down
Loading
Loading