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
138 changes: 135 additions & 3 deletions Generator/JavaClientWriter.cs
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
using Generator.Extensions;
using Newtonsoft.Json;
using Relewise.Client.Requests;
using System.CodeDom.Compiler;
using System.Reflection;

namespace Generator;

Expand Down Expand Up @@ -35,18 +37,25 @@ public void GenerateClientClass(Type clientType, string[] clientMethodNames)
.Select(derivedType => (
methodName: info.Name.ToCamelCase(),
parameterType: javaWriter.TypeName(derivedType),
parameterClrType: derivedType,
parameterName: info.GetParameters().First().Name!,
returnType: info.ReturnType))
: new[]
{
(
methodName: info.Name.ToCamelCase(),
parameterType: javaWriter.TypeName(info.GetParameters().First().ParameterType),
parameterClrType: info.GetParameters().First().ParameterType,
parameterName: info.GetParameters().First().Name!,
returnType: info.ReturnType)
})
.ToArray();

var requiresBatchingImports = clientMethods.Any(method =>
(method.parameterClrType.GetProperty("Requests")
?? method.parameterClrType.GetProperty("Items")) is { } property
&& GetCollectionElementType(property.PropertyType) is not null);


int timeout = 5;
if (clientType.GetConstructor(new[] { typeof(Guid), typeof(string), typeof(int) }) is { } constructor
Expand All @@ -62,7 +71,7 @@ public void GenerateClientClass(Type clientType, string[] clientMethodNames)
import {Constants.Namespace}.{Constants.GenerationFolderPath}.*;
import {Constants.Namespace}.infrastructure.*;
import java.io.IOException;

{(requiresBatchingImports ? "import java.util.*;\n" : "")}
""");

writer.WriteLine($"public class {clientType.Name} extends RelewiseClient");
Expand All @@ -76,7 +85,14 @@ public void GenerateClientClass(Type clientType, string[] clientMethodNames)
writer.WriteLine("");
writer.WriteLine($"public {(method.returnType == typeof(void) ? "void" : javaWriter.TypeName(method.returnType))} {method.methodName}({method.parameterType} {method.parameterName}) throws IOException, InterruptedException, ClientException {{");
writer.Indent++;
if (method.returnType == typeof(void))
var collectionProperty = method.parameterClrType.GetProperty("Requests")
?? method.parameterClrType.GetProperty("Items");

if (collectionProperty is not null && GetCollectionElementType(collectionProperty.PropertyType) is { } collectionElementType)
{
WriteBatchedMethod(writer, method.parameterClrType, method.parameterType, method.parameterName, method.returnType, collectionProperty, collectionElementType);
}
else if (method.returnType == typeof(void))
{
writer.WriteLine($"makeRequestAndValidate(\"{method.parameterType}\", {method.parameterName}, {javaWriter.TypeName(method.returnType)}.class);");
}
Expand All @@ -90,4 +106,120 @@ public void GenerateClientClass(Type clientType, string[] clientMethodNames)
writer.Indent--;
writer.WriteLine("}");
}
}

private void WriteBatchedMethod(
IndentedTextWriter writer,
Type parameterClrType,
string parameterType,
string parameterName,
Type returnType,
PropertyInfo collectionProperty,
Type collectionElementType)
{
var collectionGetter = $"{parameterName}.get{collectionProperty.Name}()";
var emptyCheck = collectionProperty.PropertyType.IsArray
? $"{collectionGetter}.length == 0"
: $"{collectionGetter}.isEmpty()";

writer.WriteLine($"if ({collectionGetter} == null || {emptyCheck}) {{");
writer.Indent++;
writer.WriteLine(returnType == typeof(void) ? "return;" : "return null;");
writer.Indent--;
writer.WriteLine("}");

if (returnType != typeof(void))
{
writer.WriteLine($"{javaWriter.TypeName(returnType)} aggregatedResponse = null;");
}

writer.WriteLine($"for (var batch : createBatches({collectionGetter})) {{");
writer.Indent++;
writer.WriteLine($"var chunkedRequest = new {parameterType}();");
foreach (var property in GetCopyableProperties(parameterClrType, collectionProperty))
{
writer.WriteLine($"chunkedRequest.set{property.Name}({CopyPropertyValue(parameterName, property)});");
}
writer.WriteLine($"chunkedRequest.set{collectionProperty.Name}(batch.toArray(new {javaWriter.TypeName(collectionElementType)}[0]));");

if (returnType == typeof(void))
{
writer.WriteLine($"makeRequestAndValidate(\"{parameterType}\", chunkedRequest, Void.class);");
}
else
{
writer.WriteLine($"var chunkResponse = makeRequestAndValidate(\"{parameterType}\", chunkedRequest, {javaWriter.TypeName(returnType)}.class);");
writer.WriteLine("if (chunkResponse == null) {");
writer.Indent++;
writer.WriteLine("continue;");
writer.Indent--;
writer.WriteLine("}");
writer.WriteLine("if (aggregatedResponse == null) {");
writer.Indent++;
writer.WriteLine("aggregatedResponse = chunkResponse;");
writer.Indent--;
writer.WriteLine("}");
writer.WriteLine("else {");
writer.Indent++;
WriteResponseAggregation(writer, returnType);
writer.Indent--;
writer.WriteLine("}");
}

writer.Indent--;
writer.WriteLine("}");

if (returnType != typeof(void))
{
writer.WriteLine("return aggregatedResponse;");
}
}

private void WriteResponseAggregation(IndentedTextWriter writer, Type returnType)
{
var responsesProperty = returnType.GetProperty("Responses");
if (responsesProperty is null || GetCollectionElementType(responsesProperty.PropertyType) is not { } responseElementType)
{
return;
}

var responseElementTypeName = javaWriter.TypeName(responseElementType);
writer.WriteLine("if (chunkResponse.getResponses() != null) {");
writer.Indent++;
writer.WriteLine("var responses = aggregatedResponse.getResponses() == null");
writer.Indent++;
writer.WriteLine($"? new ArrayList<{responseElementTypeName}>()");
writer.WriteLine(": new ArrayList<>(Arrays.asList(aggregatedResponse.getResponses()));");
writer.Indent--;
writer.WriteLine("responses.addAll(Arrays.asList(chunkResponse.getResponses()));");
writer.WriteLine($"aggregatedResponse.setResponses(responses.toArray(new {responseElementTypeName}[0]));");
writer.Indent--;
writer.WriteLine("}");
}

private string CopyPropertyValue(string parameterName, PropertyInfo property)
{
var getter = $"{parameterName}.get{property.Name}()";
return property.PropertyType.IsGenericType
&& property.PropertyType.GetGenericTypeDefinition() == typeof(List<>)
&& property.PropertyType.GenericTypeArguments is [var elementType]
? $"{getter} == null ? null : {getter}.toArray(new {javaWriter.TypeName(elementType)}[0])"
: getter;
}

private static PropertyInfo[] GetCopyableProperties(Type parameterType, PropertyInfo collectionProperty) => parameterType
.GetProperties()
.Where(info => info.Name != collectionProperty.Name
&& info.MemberType is MemberTypes.Property
&& info.GetIndexParameters().Length is 0
&& info.GetMethod is { IsAbstract: false, IsPublic: true, IsStatic: false }
&& info.SetMethod is { IsAbstract: false, IsPublic: true, IsStatic: false }
&& !Attribute.IsDefined(info, typeof(JsonIgnoreAttribute))
&& info.Name != "Custom")
.ToArray();

private static Type? GetCollectionElementType(Type type) => type.IsArray
? type.GetElementType()
: type.IsGenericType && type.GetGenericTypeDefinition() == typeof(List<>)
? type.GenericTypeArguments[0]
: null;
}
55 changes: 53 additions & 2 deletions src/src/main/java/com/relewise/client/Recommender.java
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import com.relewise.client.model.*;
import com.relewise.client.infrastructure.*;
import java.io.IOException;
import java.util.*;

public class Recommender extends RelewiseClient
{
Expand Down Expand Up @@ -126,11 +127,61 @@ public BrandRecommendationResponse recommend(BrandRecommendationRequest request)
}

public ProductRecommendationResponseCollection recommend(ProductRecommendationRequestCollection request) throws IOException, InterruptedException, ClientException {
return makeRequestAndValidate("ProductRecommendationRequestCollection", request, ProductRecommendationResponseCollection.class);
if (request.getRequests() == null || request.getRequests().isEmpty()) {
return null;
}
ProductRecommendationResponseCollection aggregatedResponse = null;
for (var batch : createBatches(request.getRequests())) {
var chunkedRequest = new ProductRecommendationRequestCollection();
chunkedRequest.setRequireDistinctProductsAcrossResults(request.getRequireDistinctProductsAcrossResults());
chunkedRequest.setRequests(batch.toArray(new ProductRecommendationRequest[0]));
var chunkResponse = makeRequestAndValidate("ProductRecommendationRequestCollection", chunkedRequest, ProductRecommendationResponseCollection.class);
if (chunkResponse == null) {
continue;
}
if (aggregatedResponse == null) {
aggregatedResponse = chunkResponse;
}
else {
if (chunkResponse.getResponses() != null) {
var responses = aggregatedResponse.getResponses() == null
? new ArrayList<ProductRecommendationResponse>()
: new ArrayList<>(Arrays.asList(aggregatedResponse.getResponses()));
responses.addAll(Arrays.asList(chunkResponse.getResponses()));
aggregatedResponse.setResponses(responses.toArray(new ProductRecommendationResponse[0]));
}
}
}
return aggregatedResponse;
}

public ContentRecommendationResponseCollection recommend(ContentRecommendationRequestCollection request) throws IOException, InterruptedException, ClientException {
return makeRequestAndValidate("ContentRecommendationRequestCollection", request, ContentRecommendationResponseCollection.class);
if (request.getRequests() == null || request.getRequests().isEmpty()) {
return null;
}
ContentRecommendationResponseCollection aggregatedResponse = null;
for (var batch : createBatches(request.getRequests())) {
var chunkedRequest = new ContentRecommendationRequestCollection();
chunkedRequest.setRequireDistinctContentsAcrossResults(request.getRequireDistinctContentsAcrossResults());
chunkedRequest.setRequests(batch.toArray(new ContentRecommendationRequest[0]));
var chunkResponse = makeRequestAndValidate("ContentRecommendationRequestCollection", chunkedRequest, ContentRecommendationResponseCollection.class);
if (chunkResponse == null) {
continue;
}
if (aggregatedResponse == null) {
aggregatedResponse = chunkResponse;
}
else {
if (chunkResponse.getResponses() != null) {
var responses = aggregatedResponse.getResponses() == null
? new ArrayList<ContentRecommendationResponse>()
: new ArrayList<>(Arrays.asList(aggregatedResponse.getResponses()));
responses.addAll(Arrays.asList(chunkResponse.getResponses()));
aggregatedResponse.setResponses(responses.toArray(new ContentRecommendationResponse[0]));
}
}
}
return aggregatedResponse;
}

public ProductRecommendationResponse recommend(ProductRecommendationRequest request) throws IOException, InterruptedException, ClientException {
Expand Down
36 changes: 35 additions & 1 deletion src/src/main/java/com/relewise/client/Searcher.java
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import com.relewise.client.model.*;
import com.relewise.client.infrastructure.*;
import java.io.IOException;
import java.util.*;

public class Searcher extends RelewiseClient
{
Expand Down Expand Up @@ -30,6 +31,39 @@ public SearchTermPredictionResponse predict(SearchTermPredictionRequest request)
}

public SearchResponseCollection batch(SearchRequestCollection request) throws IOException, InterruptedException, ClientException {
return makeRequestAndValidate("SearchRequestCollection", request, SearchResponseCollection.class);
if (request.getRequests() == null || request.getRequests().isEmpty()) {
return null;
}
SearchResponseCollection aggregatedResponse = null;
for (var batch : createBatches(request.getRequests())) {
var chunkedRequest = new SearchRequestCollection();
chunkedRequest.setLanguage(request.getLanguage());
chunkedRequest.setCurrency(request.getCurrency());
chunkedRequest.setUser(request.getUser());
chunkedRequest.setDisplayedAtLocation(request.getDisplayedAtLocation());
chunkedRequest.setRelevanceModifiers(request.getRelevanceModifiers());
chunkedRequest.setFilters(request.getFilters());
chunkedRequest.setIndexSelector(request.getIndexSelector());
chunkedRequest.setPostFilters(request.getPostFilters());
chunkedRequest.setChannel(request.getChannel());
chunkedRequest.setRequests(batch.toArray(new SearchRequest[0]));
var chunkResponse = makeRequestAndValidate("SearchRequestCollection", chunkedRequest, SearchResponseCollection.class);
if (chunkResponse == null) {
continue;
}
if (aggregatedResponse == null) {
aggregatedResponse = chunkResponse;
}
else {
if (chunkResponse.getResponses() != null) {
var responses = aggregatedResponse.getResponses() == null
? new ArrayList<SearchResponse>()
: new ArrayList<>(Arrays.asList(aggregatedResponse.getResponses()));
responses.addAll(Arrays.asList(chunkResponse.getResponses()));
aggregatedResponse.setResponses(responses.toArray(new SearchResponse[0]));
}
}
}
return aggregatedResponse;
}
}
10 changes: 9 additions & 1 deletion src/src/main/java/com/relewise/client/Tracker.java
Original file line number Diff line number Diff line change
Expand Up @@ -3,14 +3,22 @@
import com.relewise.client.model.*;
import com.relewise.client.infrastructure.*;
import java.io.IOException;
import java.util.*;

public class Tracker extends RelewiseClient
{
public Tracker(String datasetId, String apiKey, String serverUrl) { super(datasetId, apiKey, serverUrl, 5); }
public Tracker(String datasetId, String apiKey, String serverUrl, int timeout) { super(datasetId, apiKey, serverUrl, timeout); }

public void track(BatchedTrackingRequest trackingRequest) throws IOException, InterruptedException, ClientException {
makeRequestAndValidate("BatchedTrackingRequest", trackingRequest, Void.class);
if (trackingRequest.getItems() == null || trackingRequest.getItems().length == 0) {
return;
}
for (var batch : createBatches(trackingRequest.getItems())) {
var chunkedRequest = new BatchedTrackingRequest();
chunkedRequest.setItems(batch.toArray(new Trackable[0]));
makeRequestAndValidate("BatchedTrackingRequest", chunkedRequest, Void.class);
}
}

public void track(TrackBrandAdministrativeActionRequest trackingRequest) throws IOException, InterruptedException, ClientException {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,11 @@
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.time.Duration;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.Properties;
import java.util.function.Supplier;

public class RelewiseClient {
private static final String apiKeyNotDefinedMessage = "apiKey must not be empty.";
Expand All @@ -26,6 +29,7 @@ public class RelewiseClient {
private final String serverUrl;
private final int timeout;
private final ObjectMapper objectMapper;
private int batchSize = 1000;

public RelewiseClient(String datasetId, String apiKey, String serverUrl, int timeout) {
if (apiKey.isBlank()) {
Expand All @@ -51,6 +55,36 @@ public RelewiseClient(String datasetId, String apiKey, String serverUrl, int tim
}
}

/** Returns the maximum number of items sent in one batch request. */
public int getBatchSize() {
return batchSize;
}

/** Configures the maximum number of items sent in one batch request. */
public void setBatchSize(int batchSize) {
if (batchSize < 1) {
throw new IllegalArgumentException("batchSize must be greater than 0.");
}
this.batchSize = batchSize;
}

protected <T> List<List<T>> createBatches(T[] items) {
return items == null ? Collections.emptyList() : createBatches(Arrays.asList(items));
}

protected <T> List<List<T>> createBatches(List<T> items) {
if (items == null || items.isEmpty()) {
return Collections.emptyList();
}

var configuredBatchSize = batchSize;
var batches = new ArrayList<List<T>>(((items.size() - 1) / configuredBatchSize) + 1);
for (int offset = 0; offset < items.size(); offset += configuredBatchSize) {
batches.add(new ArrayList<>(items.subList(offset, Math.min(offset + configuredBatchSize, items.size()))));
}
return batches;
}

public HttpResponse<String> makeRequestAsync(String endpoint, LicensedRequest requestBody) throws IOException, InterruptedException {
var stringRequestBody = objectMapper.writeValueAsString(requestBody);

Expand Down
Loading
Loading