diff --git a/src/main/java/com/cognite/client/CogniteClient.java b/src/main/java/com/cognite/client/CogniteClient.java index 0194ba43..e212ba9a 100644 --- a/src/main/java/com/cognite/client/CogniteClient.java +++ b/src/main/java/com/cognite/client/CogniteClient.java @@ -563,6 +563,17 @@ public Transformations transformations() { return Transformations.of(this); } + /** + * Returns {@link LimitValues} representing the Cognite Limit Values API endpoints. + * + * Note: This API is currently in alpha and requires the cdf-version header. + * + * @return The limit values api object. + */ + public LimitValues limitValues() { + return LimitValues.of(this); + } + /** * Returns the services layer mirroring the Cognite Data Fusion API. * @return diff --git a/src/main/java/com/cognite/client/LimitValues.java b/src/main/java/com/cognite/client/LimitValues.java new file mode 100644 index 00000000..1207b1f1 --- /dev/null +++ b/src/main/java/com/cognite/client/LimitValues.java @@ -0,0 +1,261 @@ +/* + * Copyright (c) 2020 Cognite AS + * + * 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 com.cognite.client; + +import com.cognite.client.dto.LimitValue; +import com.cognite.client.servicesV1.ConnectorConstants; +import com.cognite.client.servicesV1.ResponseBinary; +import com.cognite.client.servicesV1.parser.LimitValueParser; +import com.cognite.client.servicesV1.util.JsonUtil; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.google.auto.value.AutoValue; +import com.google.common.base.Preconditions; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.net.URI; +import java.util.ArrayList; +import java.util.Iterator; +import java.util.List; +import java.util.Map; + +/** + * This class represents the Cognite Limit Values API endpoint. + * + * It provides methods for reading {@link LimitValue} objects. + * + * Note: This API is currently in alpha and requires the cdf-version header. + */ +@AutoValue +public abstract class LimitValues extends ApiBase { + + private static final String CDF_VERSION_HEADER = "cdf-version"; + private static final String CDF_VERSION_VALUE = "20230101-alpha"; + private static final ObjectMapper objectMapper = JsonUtil.getObjectMapperInstance(); + + private static Builder builder() { + return new AutoValue_LimitValues.Builder(); + } + + protected static final Logger LOG = LoggerFactory.getLogger(LimitValues.class); + + /** + * Constructs a new {@link LimitValues} object using the provided client configuration. + * + * This method is intended for internal use--SDK clients should always use {@link CogniteClient} + * as the entry point to this class. + * + * @param client The {@link CogniteClient} to use for configuration settings. + * @return the limit values api object. + */ + public static LimitValues of(CogniteClient client) { + return LimitValues.builder() + .setClient(client) + .build(); + } + + /** + * Retrieves a specific limit value by its ID. + * + *

Example:

+ *
+     * {@code
+     *     LimitValue limitValue = client.limitValues().retrieve("my-limit-id");
+     * }
+     * 
+ * + * @param limitId The ID of the limit value to retrieve. + * @return The retrieved {@link LimitValue}. + * @throws Exception if the retrieval fails. + */ + public LimitValue retrieve(String limitId) throws Exception { + Preconditions.checkArgument(limitId != null && !limitId.isBlank(), + "limitId cannot be null or blank."); + + String loggingPrefix = "retrieve() - "; + LOG.debug(loggingPrefix + "Retrieving limit value with id: {}", limitId); + + URI requestUri = buildUri("limits/values/" + limitId); + + ResponseBinary response = getClient().experimental().cdfHttpRequest(requestUri) + .withHeader(CDF_VERSION_HEADER, CDF_VERSION_VALUE) + .get(); + + if (!response.getResponse().isSuccessful()) { + throw new Exception("Failed to retrieve limit value. Response code: " + + response.getResponse().code() + + ", message: " + response.getResponse().message()); + } + + String responseBody = response.getResponseBodyBytes().toStringUtf8(); + return parseLimitValue(responseBody); + } + + /** + * Returns all {@link LimitValue} objects using pagination. + * + *

Example:

+ *
+     * {@code
+     *     List allLimitValues = new ArrayList<>();
+     *     client.limitValues()
+     *             .list()
+     *             .forEachRemaining(allLimitValues::addAll);
+     * }
+     * 
+ * + * @return An {@link Iterator} to page through the results. + */ + public Iterator> list() throws Exception { + return list(Request.create()); + } + + /** + * Returns {@link LimitValue} objects that match the filters set in the {@link Request}. + * + * The results are paged through / iterated over via an {@link Iterator}--the entire results set is not buffered in + * memory, but streamed in "pages" from the Cognite API. + * + *

Example:

+ *
+     * {@code
+     *     List limitValues = new ArrayList<>();
+     *     client.limitValues()
+     *             .list(Request.create()
+     *                     .withFilterParameter("prefix", Map.of(
+     *                         "property", List.of("limitId"),
+     *                         "value", "atlas.")))
+     *             .forEachRemaining(limitValues::addAll);
+     * }
+     * 
+ * + * @param requestParameters The filters to use for retrieving limit values. + * @return An {@link Iterator} to page through the results. + */ + public Iterator> list(Request requestParameters) { + return new LimitValuesIterator(requestParameters); + } + + /** + * Builds the URI for the limit values API endpoint. + * + * @param pathSegment The path segment to append (e.g., "limits/values/list") + * @return The complete URI + */ + private URI buildUri(String pathSegment) { + String baseUrl = getClient().getBaseUrl(); + String project = getClient().getProject(); + return URI.create(String.format("%s/api/v1/projects/%s/%s", + baseUrl, project, pathSegment)); + } + + /** + * Parses a JSON string to a LimitValue object. + */ + private LimitValue parseLimitValue(String json) { + try { + return LimitValueParser.parseLimitValue(json); + } catch (Exception e) { + throw new RuntimeException("Failed to parse LimitValue from JSON: " + e.getMessage(), e); + } + } + + /** + * Iterator implementation for paginated limit values listing. + */ + private class LimitValuesIterator implements Iterator> { + private final Request requestParameters; + private String cursor = null; + private boolean hasMore = true; + + public LimitValuesIterator(Request requestParameters) { + this.requestParameters = requestParameters; + } + + @Override + public boolean hasNext() { + return hasMore; + } + + @Override + public List next() { + try { + return fetchNextBatch(); + } catch (Exception e) { + throw new RuntimeException("Failed to fetch next batch of limit values", e); + } + } + + private List fetchNextBatch() throws Exception { + URI requestUri = buildUri("limits/values/list"); + + // Build request body with cursor if available + Map requestBody = new java.util.HashMap<>(requestParameters.getRequestParameters()); + + if (!requestBody.containsKey("limit")) { + requestBody.put("limit", ConnectorConstants.DEFAULT_MAX_BATCH_SIZE); + } + + if (cursor != null) { + requestBody.put("cursor", cursor); + } + + Request request = Request.create().withRequestParameters(requestBody); + + ResponseBinary response = getClient().experimental().cdfHttpRequest(requestUri) + .withRequestBody(request) + .withHeader(CDF_VERSION_HEADER, CDF_VERSION_VALUE) + .post(); + + if (!response.getResponse().isSuccessful()) { + throw new Exception("Failed to list limit values. Response code: " + + response.getResponse().code() + + ", message: " + response.getResponse().message()); + } + + String responseBody = response.getResponseBodyBytes().toStringUtf8(); + JsonNode root = objectMapper.readTree(responseBody); + + // Parse items + List results = new ArrayList<>(); + JsonNode itemsNode = root.path("items"); + if (itemsNode.isArray()) { + for (JsonNode itemNode : itemsNode) { + results.add(LimitValueParser.parseLimitValue(itemNode)); + } + } + + // Check for next cursor + JsonNode nextCursorNode = root.path("nextCursor"); + if (nextCursorNode.isTextual() && !nextCursorNode.textValue().isEmpty()) { + cursor = nextCursorNode.textValue(); + hasMore = true; + } else { + hasMore = false; + } + + return results; + } + } + + @AutoValue.Builder + abstract static class Builder extends ApiBase.Builder { + abstract LimitValues build(); + } +} + diff --git a/src/main/java/com/cognite/client/config/ResourceType.java b/src/main/java/com/cognite/client/config/ResourceType.java index 29e92109..6742a06b 100644 --- a/src/main/java/com/cognite/client/config/ResourceType.java +++ b/src/main/java/com/cognite/client/config/ResourceType.java @@ -45,5 +45,6 @@ public enum ResourceType { TRANSFORMATIONS_JOBS, TRANSFORMATIONS_JOB_METRICS, TRANSFORMATIONS_SCHEDULES, - TRANSFORMATIONS_NOTIFICATIONS; + TRANSFORMATIONS_NOTIFICATIONS, + LIMIT_VALUE; } diff --git a/src/main/java/com/cognite/client/servicesV1/parser/LimitValueParser.java b/src/main/java/com/cognite/client/servicesV1/parser/LimitValueParser.java new file mode 100644 index 00000000..f488e4e4 --- /dev/null +++ b/src/main/java/com/cognite/client/servicesV1/parser/LimitValueParser.java @@ -0,0 +1,94 @@ +/* + * Copyright (c) 2020 Cognite AS + * + * 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 com.cognite.client.servicesV1.parser; + +import com.cognite.client.dto.LimitValue; +import com.cognite.client.servicesV1.util.JsonUtil; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.google.common.collect.ImmutableMap; + +import java.util.Map; + +import static com.cognite.client.servicesV1.ConnectorConstants.MAX_LOG_ELEMENT_LENGTH; + +/** + * This class contains a set of methods to help parsing limit value objects between Cognite API representations + * (json and proto) and typed objects. + */ +public class LimitValueParser { + static final String logPrefix = "LimitValueParser - "; + static final ObjectMapper objectMapper = JsonUtil.getObjectMapperInstance(); + + /** + * Parses a limit value json string to {@code LimitValue} proto object. + * + * @param json The JSON string to parse + * @return The parsed LimitValue object + * @throws Exception if parsing fails + */ + public static LimitValue parseLimitValue(String json) throws Exception { + JsonNode root = objectMapper.readTree(json); + return parseLimitValue(root); + } + + /** + * Parses a limit value JsonNode to {@code LimitValue} proto object. + * + * This overload allows callers that already have a JsonNode to avoid + * the overhead of converting the node to a string and re-parsing. + * + * @param root The JSON node to parse + * @return The parsed LimitValue object + * @throws Exception if parsing fails + */ + public static LimitValue parseLimitValue(JsonNode root) throws Exception { + LimitValue.Builder builder = LimitValue.newBuilder(); + + // limitId is required + if (root.path("limitId").isTextual()) { + builder.setLimitId(root.get("limitId").textValue()); + } else { + throw new Exception(logPrefix + "Unable to parse attribute: limitId. Item excerpt: " + + root.toString().substring(0, Math.min(root.toString().length(), MAX_LOG_ELEMENT_LENGTH))); + } + + // value is required + if (root.path("value").isIntegralNumber()) { + builder.setValue(root.get("value").longValue()); + } else { + throw new Exception(logPrefix + "Unable to parse attribute: value. Item excerpt: " + + root.toString().substring(0, Math.min(root.toString().length(), MAX_LOG_ELEMENT_LENGTH))); + } + + return builder.build(); + } + + /** + * Builds a request item object from {@link LimitValue}. + * + * @param element The LimitValue to convert + * @return A map representing the request body + */ + public static Map toRequestInsertItem(LimitValue element) { + return ImmutableMap.builder() + .put("limitId", element.getLimitId()) + .put("value", element.getValue()) + .build(); + } +} + diff --git a/src/main/proto/limit_value.proto b/src/main/proto/limit_value.proto new file mode 100644 index 00000000..0631cedb --- /dev/null +++ b/src/main/proto/limit_value.proto @@ -0,0 +1,16 @@ +syntax = "proto3"; + +package com.cognite.client.dto; + +option java_package = "com.cognite.client.dto"; +option java_multiple_files = true; + +// LimitValue represents a limit value from the Cognite Limits API. +message LimitValue { + // The unique identifier for this limit (e.g., "functions.running_calls") + string limit_id = 1; + + // The current value of the limit + int64 value = 2; +} + diff --git a/src/test/java/com/cognite/client/LimitValuesIntegrationTest.java b/src/test/java/com/cognite/client/LimitValuesIntegrationTest.java new file mode 100644 index 00000000..3fb0cb75 --- /dev/null +++ b/src/test/java/com/cognite/client/LimitValuesIntegrationTest.java @@ -0,0 +1,192 @@ +package com.cognite.client; + +import com.cognite.client.dto.LimitValue; +import org.junit.jupiter.api.Tag; +import org.junit.jupiter.api.Test; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.time.Duration; +import java.time.Instant; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; + +import static org.junit.jupiter.api.Assertions.*; + +/** + * Integration tests for the LimitValues API. + * + * Note: These tests require a valid CDF project with the Limits API enabled. + * Set the following environment variables: + * - TEST_PROJECT: Your CDF project name + * - TEST_CLIENT_ID: OAuth client ID + * - TEST_CLIENT_SECRET: OAuth client secret + * - TEST_TENANT_ID: Azure AD tenant ID + * - TEST_HOST: CDF API host (e.g., https://api.cognitedata.com) + */ +class LimitValuesIntegrationTest { + final Logger LOG = LoggerFactory.getLogger(this.getClass()); + + @Test + @Tag("remoteCDP") + void listLimitValues() throws Exception { + Instant startInstant = Instant.now(); + String loggingPrefix = "IntegrationTest - listLimitValues() - "; + LOG.info(loggingPrefix + "Start test. Creating Cognite client."); + + CogniteClient client = TestConfigProvider.getCogniteClient(); + LOG.info(loggingPrefix + "Finished creating the Cognite client. Duration: {}", + Duration.between(startInstant, Instant.now())); + + LOG.info(loggingPrefix + "Start listing limit values."); + List listResults = new ArrayList<>(); + + try { + client.limitValues() + .list() + .forEachRemaining(listResults::addAll); + + LOG.info(loggingPrefix + "Finished listing limit values. Found {} items. Duration: {}", + listResults.size(), + Duration.between(startInstant, Instant.now())); + + // Log some sample data if available + if (!listResults.isEmpty()) { + LimitValue sample = listResults.get(0); + LOG.info(loggingPrefix + "Sample limit value - limitId: {}", sample.getLimitId()); + } + + } catch (Exception e) { + LOG.error(loggingPrefix + "Error listing limit values: {}", e.getMessage()); + throw e; + } + + // The test passes if no exception is thrown + // The actual count depends on your project's data + LOG.info(loggingPrefix + "Test completed successfully."); + } + + @Test + @Tag("remoteCDP") + void listLimitValuesWithFilter() throws Exception { + Instant startInstant = Instant.now(); + String loggingPrefix = "IntegrationTest - listLimitValuesWithFilter() - "; + LOG.info(loggingPrefix + "Start test. Creating Cognite client."); + + CogniteClient client = TestConfigProvider.getCogniteClient(); + LOG.info(loggingPrefix + "Finished creating the Cognite client. Duration: {}", + Duration.between(startInstant, Instant.now())); + + LOG.info(loggingPrefix + "First, listing limit values to derive a prefix for filtering."); + List initialResults = new ArrayList<>(); + client.limitValues() + .list(Request.create().withRootParameter("limit", 10)) + .forEachRemaining(initialResults::addAll); + + if (initialResults.isEmpty()) { + LOG.warn(loggingPrefix + "No limit values found in project. Skipping filter test."); + return; + } + + String sampleLimitId = initialResults.get(0).getLimitId(); + String prefix = sampleLimitId.contains(".") + ? sampleLimitId.substring(0, sampleLimitId.indexOf('.') + 1) + : sampleLimitId.substring(0, Math.min(5, sampleLimitId.length())); + LOG.info(loggingPrefix + "Using derived prefix: '{}'", prefix); + + LOG.info(loggingPrefix + "Start listing limit values with prefix filter."); + List filteredResults = new ArrayList<>(); + + try { + Request filterRequest = Request.create() + .withFilterParameter("prefix", Map.of( + "property", List.of("limitId"), + "value", prefix + )); + + client.limitValues() + .list(filterRequest) + .forEachRemaining(filteredResults::addAll); + + LOG.info(loggingPrefix + "Finished listing filtered limit values. Found {} items. Duration: {}", + filteredResults.size(), + Duration.between(startInstant, Instant.now())); + + // Check all results match the prefix + for (LimitValue lv : filteredResults) { + assertTrue(lv.getLimitId().startsWith(prefix), + "LimitId should start with prefix: " + prefix); + } + + } catch (Exception e) { + LOG.error(loggingPrefix + "Error listing limit values with filter: {}", e.getMessage()); + throw e; + } + + LOG.info(loggingPrefix + "Test completed successfully."); + } + + @Test + @Tag("remoteCDP") + void retrieveLimitValueById() throws Exception { + Instant startInstant = Instant.now(); + String loggingPrefix = "IntegrationTest - retrieveLimitValueById() - "; + LOG.info(loggingPrefix + "Start test. Creating Cognite client."); + + CogniteClient client = TestConfigProvider.getCogniteClient(); + LOG.info(loggingPrefix + "Finished creating the Cognite client. Duration: {}", + Duration.between(startInstant, Instant.now())); + + // First, list to get a valid limitId + LOG.info(loggingPrefix + "First, listing limit values to get a valid ID."); + List listResults = new ArrayList<>(); + + try { + client.limitValues() + .list(Request.create().withRootParameter("limit", 1)) + .forEachRemaining(listResults::addAll); + + if (listResults.isEmpty()) { + LOG.warn(loggingPrefix + "No limit values found in project. Skipping retrieve test."); + return; + } + + String limitId = listResults.get(0).getLimitId(); + LOG.info(loggingPrefix + "Found limit value with ID: {}. Now retrieving by ID.", limitId); + + // Retrieve by ID + LimitValue retrieved = client.limitValues().retrieve(limitId); + + assertNotNull(retrieved, "Retrieved limit value should not be null"); + assertEquals(limitId, retrieved.getLimitId(), "Limit IDs should match"); + + LOG.info(loggingPrefix + "Successfully retrieved limit value. Duration: {}", + Duration.between(startInstant, Instant.now())); + + } catch (Exception e) { + LOG.error(loggingPrefix + "Error in test: {}", e.getMessage()); + throw e; + } + + LOG.info(loggingPrefix + "Test completed successfully."); + } + + @Test + @Tag("remoteCDP") + void retrieveNonExistentLimitValueThrowsException() throws Exception { + String loggingPrefix = "IntegrationTest - retrieveNonExistentLimitValueThrowsException() - "; + LOG.info(loggingPrefix + "Start test. Creating Cognite client."); + + CogniteClient client = TestConfigProvider.getCogniteClient(); + + LOG.info(loggingPrefix + "Attempting to retrieve non-existent limit value."); + + assertThrows(Exception.class, () -> { + client.limitValues().retrieve("non-existent-limit-id-12345"); + }, "Should throw exception for non-existent limit ID"); + + LOG.info(loggingPrefix + "Test completed successfully - exception was thrown as expected."); + } +} +