diff --git a/src/main/java/city/makeour/moc/MocClient.java b/src/main/java/city/makeour/moc/MocClient.java index e0d34af..6baedcc 100644 --- a/src/main/java/city/makeour/moc/MocClient.java +++ b/src/main/java/city/makeour/moc/MocClient.java @@ -2,12 +2,15 @@ import java.security.InvalidKeyException; import java.security.NoSuchAlgorithmException; +import java.util.Map; +import org.springframework.web.client.RestClient; import org.springframework.web.client.RestClient.ResponseSpec; import city.makeour.moc.ngsiv2.Ngsiv2Client; import city.makeour.ngsi.v2.api.EntitiesApi; import city.makeour.ngsi.v2.invoker.ApiClient; +import city.makeour.ngsi.v2.model.UpdateExistingEntityAttributesRequest; public class MocClient { protected Ngsiv2Client client; @@ -146,4 +149,36 @@ public ResponseSpec getEntity(String entityId, String type) { return this.entities().retrieveEntityWithResponseSpec(entityId, type, null, null, "keyValues"); } + public ResponseSpec updateEntity(String id, String type, Map attributesToUpdate) { + if (id == null || id.isBlank()) throw new IllegalArgumentException("id is required"); + if (type == null || type.isBlank()) throw new IllegalArgumentException("type is required"); + if (attributesToUpdate == null) attributesToUpdate = java.util.Collections.emptyMap(); + + try { + // Existence check + this.entities() + .retrieveEntityWithResponseSpec(id, type, null, null, "keyValues") + .toEntity(Object.class); + + // Exists -> POST (keyValues 形式でそのまま送る) + return this.client.updateEntityAttributes( + id, + "application/json", + attributesToUpdate, // Object(Map) をそのまま PATCH + type, + "keyValues" + ); + + } catch (org.springframework.web.client.RestClientResponseException e) { + if (e.getStatusCode().value() != 404) throw e; + + // Not found -> create (従来通り) + java.util.Map body = new java.util.HashMap<>(); + body.put("id", id); + body.put("type", type); + body.putAll(attributesToUpdate); + return this.createEntity("application/json", body); + } + } + } diff --git a/src/main/java/city/makeour/moc/auth/srp/Helper.java b/src/main/java/city/makeour/moc/auth/srp/Helper.java index 97884dd..1db5c7a 100644 --- a/src/main/java/city/makeour/moc/auth/srp/Helper.java +++ b/src/main/java/city/makeour/moc/auth/srp/Helper.java @@ -36,6 +36,12 @@ public static byte[] padHex(BigInteger bigInt) { } public static byte[] hexStringToByteArray(String s) { + + // 奇数桁なら先頭に 0 を追加 + if (s.length() % 2 != 0) { + s = "0" + s; + } + int len = s.length(); byte[] data = new byte[len / 2]; for (int i = 0; i < len; i += 2) { diff --git a/src/main/java/city/makeour/moc/ngsiv2/Ngsiv2Client.java b/src/main/java/city/makeour/moc/ngsiv2/Ngsiv2Client.java index c9bb224..2cd025e 100644 --- a/src/main/java/city/makeour/moc/ngsiv2/Ngsiv2Client.java +++ b/src/main/java/city/makeour/moc/ngsiv2/Ngsiv2Client.java @@ -71,4 +71,71 @@ public RestClient.ResponseSpec createEntity(String contentType, Object body) { localVarReturnType); } } + + public RestClient.ResponseSpec updateEntityAttributes( + String entityId, + String contentType, + Object body, + String type, + String options + ) { + if (entityId == null) { + throw new RestClientResponseException( + "Missing the required parameter 'entityId' when calling updateEntityAttributes", + HttpStatus.BAD_REQUEST.value(), HttpStatus.BAD_REQUEST.getReasonPhrase(), + (HttpHeaders) null, (byte[]) null, (Charset) null + ); + } + if (contentType == null) { + throw new RestClientResponseException( + "Missing the required parameter 'contentType' when calling updateEntityAttributes", + HttpStatus.BAD_REQUEST.value(), HttpStatus.BAD_REQUEST.getReasonPhrase(), + (HttpHeaders) null, (byte[]) null, (Charset) null + ); + } + if (body == null) { + throw new RestClientResponseException( + "Missing the required parameter 'body' when calling updateEntityAttributes", + HttpStatus.BAD_REQUEST.value(), HttpStatus.BAD_REQUEST.getReasonPhrase(), + (HttpHeaders) null, (byte[]) null, (Charset) null + ); + } + + Map pathParams = new HashMap(); + pathParams.put("entityId", entityId); + + MultiValueMap queryParams = new LinkedMultiValueMap(); + HttpHeaders headerParams = new HttpHeaders(); + MultiValueMap cookieParams = new LinkedMultiValueMap(); + MultiValueMap formParams = new LinkedMultiValueMap(); + + // ?type=... と ?options=... + queryParams.putAll(this.apiClient.parameterToMultiValueMap(null, "type", type)); + queryParams.putAll(this.apiClient.parameterToMultiValueMap(null, "options", options)); + + headerParams.add("Content-Type", this.apiClient.parameterToString(contentType)); + + String[] localVarAccepts = new String[] { }; // 204 No Content 想定 + List localVarAccept = this.apiClient.selectHeaderAccept(localVarAccepts); + String[] localVarContentTypes = new String[] { "application/json" }; + MediaType localVarContentType = this.apiClient.selectHeaderContentType(localVarContentTypes); + String[] localVarAuthNames = new String[] { }; + + ParameterizedTypeReference localVarReturnType = new ParameterizedTypeReference<>() {}; + return this.apiClient.invokeAPI( + "/v2/entities/{entityId}/attrs", + HttpMethod.POST, + pathParams, + queryParams, + body, // ← Map 等をそのまま渡す + headerParams, + cookieParams, + formParams, + localVarAccept, + localVarContentType, + localVarAuthNames, + localVarReturnType + ); + } + } diff --git a/src/test/java/city/makeour/moc/MocClientTest.java b/src/test/java/city/makeour/moc/MocClientTest.java index acc9b68..20149b3 100644 --- a/src/test/java/city/makeour/moc/MocClientTest.java +++ b/src/test/java/city/makeour/moc/MocClientTest.java @@ -1,17 +1,21 @@ package city.makeour.moc; import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertNotNull; import java.security.GeneralSecurityException; import java.security.NoSuchAlgorithmException; +import java.util.HashMap; import java.util.List; +import java.util.Map; import java.util.UUID; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.condition.EnabledIfEnvironmentVariable; import org.junit.jupiter.api.condition.EnabledIfEnvironmentVariables; +import org.springframework.core.ParameterizedTypeReference; import city.makeour.ngsi.v2.api.EntitiesApi; import city.makeour.ngsi.v2.model.CreateEntityRequest; @@ -145,5 +149,60 @@ void testCreateAndGetEntity_Minimal() throws GeneralSecurityException, NoSuchAlg assertNotNull(retrievedEntity); assertEquals(entityId, retrievedEntity.getId()); + } + + @Test + @DisplayName("updateEntityのUpsert(作成・更新)ロジックをテストする") + @EnabledIfEnvironmentVariables({ + @EnabledIfEnvironmentVariable(named = "TEST_COGNITO_USER_POOL_ID", matches = ".*"), + @EnabledIfEnvironmentVariable(named = "TEST_COGNITO_CLIENT_ID", matches = ".*"), + @EnabledIfEnvironmentVariable(named = "TEST_COGNITO_USERNAME", matches = ".*"), + @EnabledIfEnvironmentVariable(named = "TEST_COGNITO_PASSWORD", matches = ".*") + }) + void testUpdateEntity_UpsertLogic() throws GeneralSecurityException, NoSuchAlgorithmException { + MocClient client = new MocClient(); + client.setMocAuthInfo(System.getenv("TEST_COGNITO_USER_POOL_ID"), System.getenv("TEST_COGNITO_CLIENT_ID")); + client.login(System.getenv("TEST_COGNITO_USERNAME"), System.getenv("TEST_COGNITO_PASSWORD")); + + String entityId = "urn:ngsi-ld:TestUpsert:" + UUID.randomUUID().toString(); + String entityType = "TestUpsertType"; + + // 1. "作成" (Insert) pathのテスト + // エンティティが存在しない --> catchブロックの this.createEntity + Map initialAttrs = new HashMap<>(); + initialAttrs.put("temperature", 25); + initialAttrs.put("humidity", 50); // "temperature" 以外の属性も指定 + + client.updateEntity(entityId, entityType, initialAttrs); + + // 検証 (作成) + ParameterizedTypeReference> mapType = new ParameterizedTypeReference<>() {}; + Map createdEntity = client.getEntity(entityId, entityType).body(mapType); + + assertNotNull(createdEntity); + assertEquals(entityId, createdEntity.get("id")); + assertEquals(25, createdEntity.get("temperature")); + assertEquals(50, createdEntity.get("humidity")); + + // 2. "更新" (Update/PATCH) pathのテスト + // エンティティが既に存在する --> tryブロックの updateExistingEntityAttributesWithResponseSpec + Map updateAttrs = new HashMap<>(); + updateAttrs.put("temperature", 30); // 更新 + updateAttrs.put("seatNumber", 10); // 追加 + updateAttrs.put("status", "active"); + + client.updateEntity(entityId, entityType, updateAttrs); + + // 検証 (更新) + Map updatedEntity = client.getEntity(entityId, entityType).body(mapType); + + assertNotNull(updatedEntity); + // 更新・追加されている + assertEquals(30, updatedEntity.get("temperature")); + assertEquals(10, updatedEntity.get("seatNumber")); + // 最初の作成時から変更されず残っている + assertEquals(50, updatedEntity.get("humidity")); + + assertEquals("active", updatedEntity.get("status")); } }