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
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
import software.amazon.smithy.java.core.serde.ShapeSerializer;
import software.amazon.smithy.java.core.serde.TimestampFormatter;
import software.amazon.smithy.java.http.api.HeaderName;
import software.amazon.smithy.java.io.uri.URLEncoding;
import software.amazon.smithy.model.shapes.ShapeType;
import software.amazon.smithy.model.traits.HttpTrait;
import software.amazon.smithy.utils.SmithyInternalApi;
Expand All @@ -40,6 +41,10 @@ public final class HttpBindingSchemaExtensions
*/
public static final SchemaExtensionKey<HttpBindingExt> KEY = new SchemaExtensionKey<>();

private static final Schema[] NO_SCHEMAS = new Schema[0];
private static final HeaderName[] NO_HEADER_NAMES = new HeaderName[0];
private static final String[] NO_STRINGS = new String[0];

/**
* Look up the {@link MemberBinding} for a member schema. Throws if no binding or not a member.
*/
Expand Down Expand Up @@ -87,9 +92,6 @@ static StructBindings structBindingsOf(Schema schema) {
return sb;
}

private static final Schema[] NO_SCHEMAS = new Schema[0];
private static final String[] NO_QUERY_LITERALS = new String[0];

/**
* Binding kind for a single member, derived from the traits applied to it.
*
Expand Down Expand Up @@ -356,12 +358,14 @@ ByteBuffer emptyBody(Codec codec, Schema schema) {
* Pre-computed HTTP-binding data for an operation schema.
*
* @param httpTrait the cached {@code @http} trait — saves an {@code expectTrait} call per request.
* @param queryLiterals flat array of (name, value) pairs from the URI's static query literals, or empty.
* @param queryLiteralKeys raw {@code @http} URI query literal keys (e.g. {@code ["x-id"]}).
* @param queryLiteralEntries pre-encoded {@code "key=value"} strings parallel to {@link #queryLiteralKeys}.
* @param defaultResponseStatus default response status declared by the {@code @http} trait.
*/
record OperationBinding(
HttpTrait httpTrait,
String[] queryLiterals,
String[] queryLiteralKeys,
String[] queryLiteralEntries,
int defaultResponseStatus) implements HttpBindingExt {}

@Override
Expand Down Expand Up @@ -732,9 +736,6 @@ private static Schema[] toArray(List<Schema> list) {
return list.isEmpty() ? NO_SCHEMAS : list.toArray(new Schema[0]);
}

private static final HeaderName[] NO_HEADER_NAMES = new HeaderName[0];
private static final String[] NO_STRINGS = new String[0];

/**
* Pre-resolve canonical {@link HeaderName}s parallel to a {@code Schema[]} of
* list-header members. Reading the name later in the deserializer becomes a
Expand Down Expand Up @@ -772,21 +773,31 @@ private static OperationBinding forOperation(Schema schema) {
var httpTrait = schema.expectTrait(TraitKey.HTTP_TRAIT);
var uriPattern = httpTrait.getUri();

// Flatten query literals into a (name, value) pair array. The trait's own map iteration is stable for a
// given trait instance, but this avoids the per-call iterator + Map.Entry allocations.
// Pre-encode static @http URI query literals into "key=value" strings so the request hot path can append
// them verbatim instead of re-encoding per call.
var queryLiteralMap = uriPattern.getQueryLiterals();
String[] queryLiterals;
String[] queryLiteralKeys;
String[] queryLiteralEntries;
if (queryLiteralMap.isEmpty()) {
queryLiterals = NO_QUERY_LITERALS;
queryLiteralKeys = NO_STRINGS;
queryLiteralEntries = NO_STRINGS;
} else {
queryLiterals = new String[2 * queryLiteralMap.size()];
int n = queryLiteralMap.size();
queryLiteralKeys = new String[n];
queryLiteralEntries = new String[n];
int i = 0;
StringBuilder pair = new StringBuilder();
for (var entry : queryLiteralMap.entrySet()) {
queryLiterals[i++] = entry.getKey();
queryLiterals[i++] = entry.getValue();
pair.setLength(0);
URLEncoding.encodeUnreserved(entry.getKey(), pair, false);
pair.append('=');
URLEncoding.encodeUnreserved(entry.getValue(), pair, false);
queryLiteralKeys[i] = entry.getKey();
queryLiteralEntries[i] = pair.toString();
i++;
}
}

return new OperationBinding(httpTrait, queryLiterals, httpTrait.getCode());
return new OperationBinding(httpTrait, queryLiteralKeys, queryLiteralEntries, httpTrait.getCode());
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -144,12 +144,13 @@ public void writeStruct(Schema schema, SerializableStruct struct) {

headers = HttpHeaders.ofModifiable(headerCount);

// Add fixed query string parameters from @http trait's uri field.
String[] qLits = operationBinding.queryLiterals();
if (qLits.length > 0) {
// Append the static @http URI query literals
String[] qKeys = operationBinding.queryLiteralKeys();
if (qKeys.length > 0) {
QueryStringBuilder qsb = queryStringParams();
for (int i = 0; i < qLits.length; i += 2) {
qsb.add(qLits[i], qLits[i + 1]);
String[] qEntries = operationBinding.queryLiteralEntries();
for (int i = 0; i < qKeys.length; i++) {
qsb.addPreEncoded(qKeys[i], qEntries[i]);
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,23 @@ public void addForQueryParams(String key, String value) {
}
}

/**
* Append a pre-encoded {@code "key=value"} entry to the query string and register its raw key with the
* {@code @httpQuery} dedupe set so a subsequent {@link #addForQueryParams} with the same key will be skipped.
*
* @param rawKey raw (unencoded) key, used only for the dedupe set.
* @param encodedKeyEqValue {@code "encodedKey=encodedValue"} as a single ready-to-append string.
*/
public void addPreEncoded(String rawKey, String encodedKeyEqValue) {
if (!empty) {
builder.append('&');
} else {
empty = false;
}
builder.append(encodedKeyEqValue);
httpQueryKeys.add(rawKey);
}

private void append(String key, String value) {
if (!empty) {
builder.append('&');
Expand Down