Skip to content

Commit afaf4c1

Browse files
Added Blob DELETE and PATCH endpoints (#1090)
Fixes #1050 Adds delete and update endpoints to blob controller. Added integration test for update and deletion. --------- Co-authored-by: rma-rripken <89810919+rma-rripken@users.noreply.github.com>
1 parent 8a0a35a commit afaf4c1

3 files changed

Lines changed: 237 additions & 30 deletions

File tree

cwms-data-api/src/main/java/cwms/cda/api/BlobController.java

Lines changed: 69 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,11 @@
11
package cwms.cda.api;
22

3+
import static com.codahale.metrics.MetricRegistry.name;
4+
import static cwms.cda.api.Controllers.*;
5+
36
import com.codahale.metrics.Histogram;
47
import com.codahale.metrics.MetricRegistry;
58
import com.codahale.metrics.Timer;
6-
import com.google.common.flogger.FluentLogger;
79
import cwms.cda.api.errors.CdaError;
810
import cwms.cda.data.dao.BlobDao;
911
import cwms.cda.data.dao.JooqDao;
@@ -23,23 +25,19 @@
2325
import io.javalin.plugin.openapi.annotations.OpenApiParam;
2426
import io.javalin.plugin.openapi.annotations.OpenApiRequestBody;
2527
import io.javalin.plugin.openapi.annotations.OpenApiResponse;
26-
import org.jetbrains.annotations.NotNull;
27-
import org.jooq.DSLContext;
28-
29-
import javax.servlet.http.HttpServletResponse;
3028
import java.io.InputStream;
3129
import java.util.List;
3230
import java.util.Optional;
31+
import javax.servlet.http.HttpServletResponse;
32+
import org.jetbrains.annotations.NotNull;
33+
import org.jooq.DSLContext;
3334

34-
import static com.codahale.metrics.MetricRegistry.name;
35-
import static cwms.cda.api.Controllers.*;
3635

3736

3837
/**
3938
*
4039
*/
4140
public class BlobController implements CrudHandler {
42-
private static final FluentLogger logger = FluentLogger.forEnclosingClass();
4341
private static final int DEFAULT_PAGE_SIZE = 20;
4442
public static final String TAG = "Blob";
4543

@@ -130,6 +128,8 @@ public void getAll(@NotNull Context ctx) {
130128
}
131129

132130
@OpenApi(
131+
description = "Returns the binary value of the requested blob as a seekable stream with the "
132+
+ "appropriate media type.",
133133
queryParams = {
134134
@OpenApiParam(name = OFFICE, description = "Specifies the owning office."),
135135
},
@@ -193,16 +193,70 @@ public void create(@NotNull Context ctx) {
193193
}
194194
}
195195

196-
@OpenApi(ignore = true)
196+
@OpenApi(
197+
description = "Update an existing Blob",
198+
pathParams = {
199+
@OpenApiParam(name = BLOB_ID, description = "The blob identifier to be deleted"),
200+
},
201+
requestBody = @OpenApiRequestBody(
202+
content = {
203+
@OpenApiContent(from = Blob.class, type = Formats.JSONV2),
204+
@OpenApiContent(from = Blob.class, type = Formats.JSON)
205+
},
206+
required = true),
207+
method = HttpMethod.POST,
208+
tags = {TAG}
209+
)
197210
@Override
198-
public void update(Context ctx, @NotNull String blobId) {
199-
ctx.status(HttpCode.NOT_IMPLEMENTED).json(CdaError.notImplemented());
211+
public void update(@NotNull Context ctx, @NotNull String blobId) {
212+
try (final Timer.Context ignored = markAndTime(CREATE)) {
213+
DSLContext dsl = getDslContext(ctx);
214+
215+
String reqContentType = ctx.req.getContentType();
216+
String formatHeader = reqContentType != null ? reqContentType : Formats.JSON;
217+
218+
ContentType contentType = Formats.parseHeader(formatHeader, Blob.class);
219+
Blob blob = Formats.parseContent(contentType, ctx.bodyAsInputStream(), Blob.class);
220+
221+
if (blob.getOfficeId() == null) {
222+
throw new FormattingException("An officeId is required when updating a blob");
223+
}
224+
225+
if (blob.getId() == null) {
226+
throw new FormattingException("An Id is required when updating a blob");
227+
}
228+
229+
if (blob.getValue() == null) {
230+
throw new FormattingException("A non-empty value field is required when "
231+
+ "updating a blob");
232+
}
233+
234+
BlobDao dao = new BlobDao(dsl);
235+
dao.update(blob, false);
236+
ctx.status(HttpServletResponse.SC_OK);
237+
}
200238
}
201239

202-
@OpenApi(ignore = true)
240+
@OpenApi(
241+
description = "Deletes requested blob",
242+
pathParams = {
243+
@OpenApiParam(name = BLOB_ID, description = "The blob identifier to be deleted"),
244+
},
245+
queryParams = {
246+
@OpenApiParam(name = OFFICE, required = true, description = "Specifies the "
247+
+ "owning office of the blob to be deleted"),
248+
},
249+
method = HttpMethod.DELETE,
250+
tags = {TAG}
251+
)
203252
@Override
204-
public void delete(Context ctx, @NotNull String blobId) {
205-
ctx.status(HttpCode.NOT_IMPLEMENTED).json(CdaError.notImplemented());
253+
public void delete(@NotNull Context ctx, @NotNull String blobId) {
254+
try (Timer.Context ignored = markAndTime(DELETE)) {
255+
DSLContext dsl = getDslContext(ctx);
256+
String office = requiredParam(ctx, OFFICE);
257+
BlobDao dao = new BlobDao(dsl);
258+
dao.delete(office, blobId);
259+
ctx.status(HttpServletResponse.SC_NO_CONTENT);
260+
}
206261
}
207-
208262
}

cwms-data-api/src/main/java/cwms/cda/data/dao/BlobDao.java

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -150,6 +150,55 @@ public void create(Blob blob, boolean failIfExists, boolean ignoreNulls) {
150150
blob.getOfficeId()));
151151
}
152152

153+
public void update(Blob blob, boolean ignoreNulls) {
154+
String pFailIfExists = formatBool(false);
155+
String pIgnoreNulls = formatBool(ignoreNulls);
156+
157+
if(blob == null){
158+
throw new NotFoundException("Null blob provided to update");
159+
}
160+
161+
if (!blobExists(blob.getOfficeId(), blob.getId())) {
162+
throw new NotFoundException("Unable to find blob with id " + blob.getId() + " in office " + blob.getOfficeId());
163+
}
164+
165+
dsl.connection(c -> CWMS_TEXT_PACKAGE.call_STORE_BINARY(
166+
getDslContext(c, blob.getOfficeId()).configuration(),
167+
blob.getValue(),
168+
blob.getId(),
169+
blob.getMediaTypeId(),
170+
blob.getDescription(),
171+
pFailIfExists,
172+
pIgnoreNulls,
173+
blob.getOfficeId()));
174+
}
175+
176+
public void delete(String office, String id) {
177+
if (!blobExists(office, id)) {
178+
throw new NotFoundException("Unable to find blob with id " + id + " in office " + office);
179+
}
180+
dsl.connection(c -> CWMS_TEXT_PACKAGE.call_DELETE_BINARY(
181+
getDslContext(c, office).configuration(),
182+
id,
183+
office));
184+
}
185+
186+
private boolean blobExists(String office, String id) {
187+
String existsQuery = "select 1 "
188+
+ "from CWMS_20.AT_BLOB \n"
189+
+ "join CWMS_20.CWMS_OFFICE on AT_BLOB.OFFICE_CODE = CWMS_OFFICE.OFFICE_CODE \n"
190+
+ "WHERE ID = ? AND upper(CWMS_OFFICE.OFFICE_ID) = upper(?)";
191+
return connectionResult(dsl, conn -> {
192+
try (PreparedStatement preparedStatement = conn.prepareStatement(existsQuery)) {
193+
preparedStatement.setString(1, id);
194+
preparedStatement.setString(2, office);
195+
try (ResultSet resultSet = preparedStatement.executeQuery()) {
196+
return resultSet.next();
197+
}
198+
}
199+
});
200+
}
201+
153202

154203
public static byte[] readFully(@NotNull InputStream stream) throws IOException {
155204
byte[] buffer = new byte[8192];

cwms-data-api/src/test/java/cwms/cda/api/BlobControllerTestIT.java

Lines changed: 119 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -40,20 +40,20 @@ static void createExistingBlob() throws Exception
4040
TestAccounts.KeyUser user = TestAccounts.KeyUser.SPK_NORMAL;
4141

4242
given()
43-
.log().ifValidationFails(LogDetail.ALL,true)
44-
.contentType(Formats.JSONV2)
45-
.body(serializedBlob)
46-
.header("Authorization",user.toHeaderValue())
47-
.queryParam("office",SPK)
48-
.queryParam("fail-if-exists",false)
49-
.when()
50-
.redirects().follow(true)
51-
.redirects().max(3)
52-
.post("/blobs/")
53-
.then()
54-
.log().ifValidationFails(LogDetail.ALL,true)
55-
.assertThat()
56-
.statusCode(is(HttpServletResponse.SC_CREATED));
43+
.log().ifValidationFails(LogDetail.ALL,true)
44+
.contentType(Formats.JSONV2)
45+
.body(serializedBlob)
46+
.header("Authorization",user.toHeaderValue())
47+
.queryParam("office",SPK)
48+
.queryParam("fail-if-exists",false)
49+
.when()
50+
.redirects().follow(true)
51+
.redirects().max(3)
52+
.post("/blobs/")
53+
.then()
54+
.log().ifValidationFails(LogDetail.ALL,true)
55+
.assertThat()
56+
.statusCode(is(HttpServletResponse.SC_CREATED));
5757
}
5858

5959
@Test
@@ -75,7 +75,7 @@ void test_getOne_not_found() throws UnsupportedEncodingException {
7575

7676

7777
@Test
78-
void test_create_getOne() throws JsonProcessingException
78+
void test_create_getOne()
7979
{
8080
/* There is an issue with how javalin handles / in the path that are actually part
8181
of the object name (NOTE: good candidate for actually having a GUID or other "code"
@@ -128,6 +128,110 @@ void test_blob_range()
128128
.body( is("t value"));
129129
}
130130

131+
@Test
132+
void testCreateUpdateDelete() throws Exception
133+
{
134+
String blobId = "TEST_BLOBIT_ID";
135+
String blobValue = "testing value";
136+
String origDesc = "testing description";
137+
byte[] origBytes = blobValue.getBytes();
138+
139+
String mediaType = "application/octet-stream";
140+
Blob blob = new Blob(SPK, blobId, origDesc, mediaType, origBytes);
141+
ObjectMapper om = JsonV2.buildObjectMapper();
142+
String serializedBlob = om.writeValueAsString(blob);
143+
TestAccounts.KeyUser user = TestAccounts.KeyUser.SPK_NORMAL;
144+
145+
// Store new blob
146+
given()
147+
.log().ifValidationFails(LogDetail.ALL,true)
148+
.contentType(Formats.JSONV2)
149+
.body(serializedBlob)
150+
.header("Authorization", user.toHeaderValue())
151+
.queryParam(Controllers.OFFICE, SPK)
152+
.queryParam(Controllers.FAIL_IF_EXISTS,false)
153+
.when()
154+
.redirects().follow(true)
155+
.redirects().max(3)
156+
.post("/blobs/")
157+
.then()
158+
.log().ifValidationFails(LogDetail.ALL,true)
159+
.assertThat()
160+
.statusCode(is(HttpServletResponse.SC_CREATED));
161+
162+
// retrieve blob, assert value matches expected
163+
given()
164+
.log()
165+
.ifValidationFails(LogDetail.ALL, true)
166+
.queryParam(Controllers.OFFICE, SPK)
167+
.when()
168+
.get("/blobs/" + blobId)
169+
.then()
170+
.log().ifValidationFails(LogDetail.ALL, true)
171+
.assertThat()
172+
.statusCode(is(HttpServletResponse.SC_OK))
173+
.body(is(blobValue));
174+
175+
// update blob
176+
String newDescription = "new description";
177+
String newBlobValue = "new blob value";
178+
byte[] newBlobBytes = newBlobValue.getBytes();
179+
blob = new Blob(SPK, blobId, newDescription, mediaType, newBlobBytes);
180+
serializedBlob = om.writeValueAsString(blob);
181+
182+
given()
183+
.log().ifValidationFails(LogDetail.ALL,true)
184+
.contentType(Formats.JSONV2)
185+
.body(serializedBlob)
186+
.header("Authorization", user.toHeaderValue())
187+
.when()
188+
.redirects().follow(true)
189+
.redirects().max(3)
190+
.patch("/blobs/" + blobId)
191+
.then()
192+
.log().ifValidationFails(LogDetail.ALL,true)
193+
.assertThat()
194+
.statusCode(is(HttpServletResponse.SC_OK));
195+
196+
// retrieve blob, assert value matches expected
197+
given()
198+
.log()
199+
.ifValidationFails(LogDetail.ALL, true)
200+
.queryParam(Controllers.OFFICE, SPK)
201+
.when()
202+
.get("/blobs/" + blobId)
203+
.then()
204+
.log().ifValidationFails(LogDetail.ALL, true)
205+
.assertThat()
206+
.statusCode(is(HttpServletResponse.SC_OK))
207+
.body(is(newBlobValue));
208+
209+
// delete blob
210+
given()
211+
.log()
212+
.ifValidationFails(LogDetail.ALL, true)
213+
.queryParam(Controllers.OFFICE, SPK)
214+
.header("Authorization", user.toHeaderValue())
215+
.when()
216+
.delete("/blobs/" + blobId)
217+
.then()
218+
.log().ifValidationFails(LogDetail.ALL, true)
219+
.assertThat()
220+
.statusCode(is(HttpServletResponse.SC_NO_CONTENT));
221+
222+
// retrieve blob, assert not found
223+
given()
224+
.log()
225+
.ifValidationFails(LogDetail.ALL, true)
226+
.queryParam(Controllers.OFFICE, SPK)
227+
.when()
228+
.get("/blobs/" + blobId)
229+
.then()
230+
.log().ifValidationFails(LogDetail.ALL, true)
231+
.assertThat()
232+
.statusCode(is(HttpServletResponse.SC_NOT_FOUND));
233+
}
234+
131235
@ParameterizedTest
132236
@EnumSource(GetAllTest.class)
133237
void test_blob_get_all_default_alias(GetAllTest test)

0 commit comments

Comments
 (0)