Skip to content

Commit ad0f6ef

Browse files
authored
Merge branch 'main' into describe-attachment-attributes
2 parents cf4c876 + a7bed3d commit ad0f6ef

File tree

4 files changed

+612
-45
lines changed

4 files changed

+612
-45
lines changed

src/main/java/org/tailormap/api/controller/AttachmentsController.java

Lines changed: 114 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,9 @@
99
import java.io.IOException;
1010
import java.io.Serializable;
1111
import java.lang.invoke.MethodHandles;
12+
import java.nio.ByteBuffer;
1213
import java.sql.SQLException;
14+
import java.util.List;
1315
import java.util.Set;
1416
import java.util.UUID;
1517
import org.geotools.api.data.Query;
@@ -74,7 +76,7 @@ public AttachmentsController(EditUtil editUtil, FeatureSourceFactoryHelper featu
7476
*/
7577
@PutMapping(
7678
path = {
77-
"${tailormap-api.base-path}/{viewerKind}/{viewerName}/layer/{appLayerId}/feature/{featureId}/attachment"
79+
"${tailormap-api.base-path}/{viewerKind}/{viewerName}/layer/{appLayerId}/feature/{featureId}/attachments"
7880
},
7981
consumes = MediaType.MULTIPART_FORM_DATA_VALUE,
8082
produces = MediaType.APPLICATION_JSON_VALUE)
@@ -92,10 +94,12 @@ public ResponseEntity<Serializable> addAttachment(
9294

9395
TMFeatureType tmFeatureType = editUtil.getEditableFeatureType(application, appTreeLayerNode, service, layer);
9496

97+
checkFeatureExists(tmFeatureType, featureId);
98+
9599
Set<@Valid AttachmentAttributeType> attachmentAttrSet =
96100
tmFeatureType.getSettings().getAttachmentAttributes();
97101
if (attachmentAttrSet == null || attachmentAttrSet.isEmpty()) {
98-
throw new ResponseStatusException(HttpStatus.BAD_REQUEST, "Feature type does not support attachments");
102+
throw new ResponseStatusException(HttpStatus.BAD_REQUEST, "Layer does not support attachments");
99103
}
100104

101105
AttachmentAttributeType attachmentAttributeType = attachmentAttrSet.stream()
@@ -107,18 +111,14 @@ public ResponseEntity<Serializable> addAttachment(
107111
.findFirst()
108112
.orElseThrow(() -> new ResponseStatusException(
109113
HttpStatus.BAD_REQUEST,
110-
"Feature type does not support attachments for attribute "
114+
"Layer does not support attachments for attribute "
111115
+ attachment.getAttributeName()
112116
+ " with mime type "
113117
+ attachment.getMimeType()
114118
+ " and size "
115119
+ fileData.length));
116120
logger.debug("Using attachment attribute {}", attachmentAttributeType);
117121

118-
if (!checkFeatureExists(tmFeatureType, featureId)) {
119-
throw new ResponseStatusException(HttpStatus.NOT_FOUND, "Feature with id " + featureId + " does not exist");
120-
}
121-
122122
AttachmentMetadata response;
123123
try {
124124
response = AttachmentsHelper.insertAttachment(tmFeatureType, attachment, featureId, fileData);
@@ -129,32 +129,120 @@ public ResponseEntity<Serializable> addAttachment(
129129
return new ResponseEntity<>(response, HttpStatus.CREATED);
130130
}
131131

132-
@DeleteMapping(path = "${tailormap-api.base-path}/attachment/{attachmentId}")
133-
public ResponseEntity<Serializable> deleteAttachment(@PathVariable UUID attachmentId) {
132+
/**
133+
* List attachments for a feature.
134+
*
135+
* @param appTreeLayerNode the application tree layer node
136+
* @param service the geo service
137+
* @param layer the geo service layer
138+
* @param application the application
139+
* @param featureId the feature id
140+
* @return the response entity containing a list of attachment metadata
141+
*/
142+
@GetMapping(
143+
path = {
144+
"${tailormap-api.base-path}/{viewerKind}/{viewerName}/layer/{appLayerId}/feature/{featureId}/attachments"
145+
},
146+
produces = MediaType.APPLICATION_JSON_VALUE)
147+
@Transactional
148+
public ResponseEntity<List<AttachmentMetadata>> listAttachments(
149+
@ModelAttribute AppTreeLayerNode appTreeLayerNode,
150+
@ModelAttribute GeoService service,
151+
@ModelAttribute GeoServiceLayer layer,
152+
@ModelAttribute Application application,
153+
@PathVariable String featureId) {
154+
155+
TMFeatureType tmFeatureType = editUtil.getEditableFeatureType(application, appTreeLayerNode, service, layer);
156+
157+
checkFeatureTypeSupportsAttachments(tmFeatureType);
158+
checkFeatureExists(tmFeatureType, featureId);
159+
160+
List<AttachmentMetadata> response;
161+
try {
162+
response = AttachmentsHelper.listAttachmentsForFeature(tmFeatureType, featureId);
163+
} catch (IOException | SQLException e) {
164+
throw new ResponseStatusException(HttpStatus.INTERNAL_SERVER_ERROR, e.getMessage());
165+
}
166+
167+
return new ResponseEntity<>(response, HttpStatus.OK);
168+
}
169+
170+
@DeleteMapping(
171+
path = "${tailormap-api.base-path}/{viewerKind}/{viewerName}/layer/{appLayerId}/attachment/{attachmentId}")
172+
@Transactional
173+
public ResponseEntity<Serializable> deleteAttachment(
174+
@ModelAttribute AppTreeLayerNode appTreeLayerNode,
175+
@ModelAttribute GeoService service,
176+
@ModelAttribute GeoServiceLayer layer,
177+
@ModelAttribute Application application,
178+
@PathVariable UUID attachmentId) {
134179
editUtil.checkEditAuthorisation();
135180

136-
logger.debug("TODO: Deleting attachment with id {}", attachmentId);
137-
throw new UnsupportedOperationException("Not implemented yet");
181+
TMFeatureType tmFeatureType = editUtil.getEditableFeatureType(application, appTreeLayerNode, service, layer);
182+
183+
checkFeatureTypeSupportsAttachments(tmFeatureType);
184+
185+
try {
186+
AttachmentsHelper.deleteAttachment(attachmentId, tmFeatureType);
187+
} catch (IOException | SQLException e) {
188+
throw new ResponseStatusException(HttpStatus.INTERNAL_SERVER_ERROR, e.getMessage());
189+
}
190+
191+
return new ResponseEntity<>(HttpStatus.NO_CONTENT);
138192
}
139193

194+
@Transactional
140195
@GetMapping(
141-
path = "${tailormap-api.base-path}/attachment/{attachmentId}",
196+
path = "${tailormap-api.base-path}/{viewerKind}/{viewerName}/layer/{appLayerId}/attachment/{attachmentId}",
142197
produces = {"application/octet-stream"})
143-
// TODO determine return type: ResponseEntity<byte[]> or ResponseEntity<InputStreamResource>?
144-
public ResponseEntity<byte[]> getAttachment(@PathVariable UUID attachmentId) {
145-
logger.debug("TODO: Getting attachment with id {}", attachmentId);
198+
public ResponseEntity<byte[]> getAttachment(
199+
@ModelAttribute AppTreeLayerNode appTreeLayerNode,
200+
@ModelAttribute GeoService service,
201+
@ModelAttribute GeoServiceLayer layer,
202+
@ModelAttribute Application application,
203+
@PathVariable UUID attachmentId) {
146204

147-
throw new UnsupportedOperationException("Not implemented yet");
205+
TMFeatureType tmFeatureType = editUtil.getEditableFeatureType(application, appTreeLayerNode, service, layer);
206+
207+
try {
208+
final AttachmentsHelper.AttachmentWithBinary attachmentWithBinary =
209+
AttachmentsHelper.getAttachment(tmFeatureType, attachmentId);
210+
211+
if (attachmentWithBinary == null) {
212+
throw new ResponseStatusException(
213+
HttpStatus.NOT_FOUND, "Attachment %s not found".formatted(attachmentId.toString()));
214+
}
215+
216+
// the binary attachment() is a read-only ByteBuffer, so we cant use .array()
217+
final ByteBuffer bb = attachmentWithBinary.attachment().asReadOnlyBuffer();
218+
bb.rewind();
219+
byte[] attachmentData = new byte[bb.remaining()];
220+
bb.get(attachmentData);
221+
222+
return ResponseEntity.ok()
223+
.header(
224+
"Content-Disposition",
225+
"inline; filename=\""
226+
+ attachmentWithBinary.attachmentMetadata().getFileName() + "\"")
227+
.contentType(MediaType.parseMediaType(
228+
attachmentWithBinary.attachmentMetadata().getMimeType()))
229+
.body(attachmentData);
230+
} catch (SQLException | IOException e) {
231+
throw new ResponseStatusException(HttpStatus.INTERNAL_SERVER_ERROR, e.getMessage());
232+
}
148233
}
149234

150-
private boolean checkFeatureExists(TMFeatureType tmFeatureType, String featureId) {
235+
private void checkFeatureExists(TMFeatureType tmFeatureType, String featureId) throws ResponseStatusException {
151236
final Filter fidFilter = ff.id(ff.featureId(featureId));
152237
SimpleFeatureSource fs = null;
153238
try {
154239
fs = featureSourceFactoryHelper.openGeoToolsFeatureSource(tmFeatureType);
155240
Query query = new Query();
156241
query.setFilter(fidFilter);
157-
return fs.getCount(query) > 0;
242+
if (fs.getCount(query) < 1) {
243+
throw new ResponseStatusException(
244+
HttpStatus.NOT_FOUND, "Feature with id " + featureId + " does not exist");
245+
}
158246
} catch (IOException e) {
159247
throw new ResponseStatusException(HttpStatus.INTERNAL_SERVER_ERROR, e.getMessage());
160248
} finally {
@@ -163,4 +251,12 @@ private boolean checkFeatureExists(TMFeatureType tmFeatureType, String featureId
163251
}
164252
}
165253
}
254+
255+
private void checkFeatureTypeSupportsAttachments(TMFeatureType tmFeatureType) throws ResponseStatusException {
256+
Set<@Valid AttachmentAttributeType> attachmentAttrSet =
257+
tmFeatureType.getSettings().getAttachmentAttributes();
258+
if (attachmentAttrSet == null || attachmentAttrSet.isEmpty()) {
259+
throw new ResponseStatusException(HttpStatus.BAD_REQUEST, "Layer does not support attachments");
260+
}
261+
}
166262
}

src/main/java/org/tailormap/api/geotools/featuresources/AttachmentsHelper.java

Lines changed: 159 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
import java.text.MessageFormat;
1919
import java.time.OffsetDateTime;
2020
import java.time.ZoneId;
21+
import java.util.ArrayList;
2122
import java.util.List;
2223
import java.util.Locale;
2324
import java.util.UUID;
@@ -244,7 +245,8 @@ private static String getCreateAttachmentsForFeatureTypeStatements(
244245
typeModifier = getValidModifier(fkColumnType, fkColumnSize);
245246
}
246247
logger.debug(
247-
"Creating attachment table for feature type with primary key {} (native type: {}, meta type: {}, size: {} (modifier: {}))",
248+
"Creating attachment table for feature type with primary key {} (native type: {}, meta type: {}, size:"
249+
+ " {} (modifier: {}))",
248250
pkDescriptor.getLocalName(),
249251
fkColumnType,
250252
pkDescriptor.getUserData().get("org.geotools.jdbc.nativeTypeName"),
@@ -388,21 +390,25 @@ public static AttachmentMetadata insertAttachment(
388390
""",
389391
featureType.getName());
390392

391-
byte[] attachmentIdBytes = asBytes(attachment.getAttachmentId());
392-
393393
JDBCDataStore ds = null;
394394
try {
395395
ds = (JDBCDataStore) new JDBCFeatureSourceHelper().createDataStore(featureType.getFeatureSource());
396+
397+
Class<?> typeOfPK = ds.getSchema(featureType.getName())
398+
.getDescriptor(featureType.getPrimaryKeyAttribute())
399+
.getType()
400+
.getBinding();
401+
396402
try (Connection conn = ds.getDataSource().getConnection();
397403
PreparedStatement stmt = conn.prepareStatement(insertSql)) {
398404

399-
stmt.setString(1, featureId);
405+
stmt.setObject(1, featureId, ds.getMapping(typeOfPK));
400406
if (featureType
401407
.getFeatureSource()
402408
.getJdbcConnection()
403409
.getDbtype()
404410
.equals(JDBCConnectionProperties.DbtypeEnum.ORACLE)) {
405-
stmt.setBytes(2, attachmentIdBytes);
411+
stmt.setBytes(2, asBytes(attachment.getAttachmentId()));
406412
} else {
407413
stmt.setObject(2, attachment.getAttachmentId());
408414
}
@@ -426,4 +432,152 @@ public static AttachmentMetadata insertAttachment(
426432
}
427433
}
428434
}
435+
436+
public static void deleteAttachment(UUID attachmentId, TMFeatureType featureType) throws IOException, SQLException {
437+
String deleteSql = MessageFormat.format(
438+
"""
439+
DELETE FROM {0}_attachments WHERE attachment_id = ?
440+
""", featureType.getName());
441+
JDBCDataStore ds = null;
442+
try {
443+
ds = (JDBCDataStore) new JDBCFeatureSourceHelper().createDataStore(featureType.getFeatureSource());
444+
try (Connection conn = ds.getDataSource().getConnection();
445+
PreparedStatement stmt = conn.prepareStatement(deleteSql)) {
446+
if (featureType
447+
.getFeatureSource()
448+
.getJdbcConnection()
449+
.getDbtype()
450+
.equals(JDBCConnectionProperties.DbtypeEnum.ORACLE)) {
451+
stmt.setBytes(1, asBytes(attachmentId));
452+
} else {
453+
stmt.setObject(1, attachmentId);
454+
}
455+
456+
stmt.executeUpdate();
457+
}
458+
} finally {
459+
if (ds != null) {
460+
ds.dispose();
461+
}
462+
}
463+
}
464+
465+
public static List<AttachmentMetadata> listAttachmentsForFeature(TMFeatureType featureType, String featureId)
466+
throws IOException, SQLException {
467+
468+
String querySql = MessageFormat.format(
469+
"""
470+
SELECT
471+
{0}_pk,
472+
attachment_id,
473+
file_name,
474+
attribute_name,
475+
description,
476+
attachment_size,
477+
mime_type,
478+
created_at,
479+
created_by
480+
FROM {0}_attachments WHERE {0}_pk = ?
481+
""",
482+
featureType.getName());
483+
484+
List<AttachmentMetadata> attachments = new ArrayList<>();
485+
JDBCDataStore ds = null;
486+
try {
487+
ds = (JDBCDataStore) new JDBCFeatureSourceHelper().createDataStore(featureType.getFeatureSource());
488+
try (Connection conn = ds.getDataSource().getConnection();
489+
PreparedStatement stmt = conn.prepareStatement(querySql)) {
490+
491+
stmt.setString(1, featureId);
492+
493+
try (ResultSet rs = stmt.executeQuery()) {
494+
while (rs.next()) {
495+
AttachmentMetadata a = new AttachmentMetadata();
496+
// attachment_id (handle UUID, RAW(16) as byte[] or string)
497+
Object idObj = rs.getObject("attachment_id");
498+
if (idObj instanceof UUID u) {
499+
a.setAttachmentId(u);
500+
} else if (idObj instanceof byte[] b) {
501+
ByteBuffer bb = ByteBuffer.wrap(b);
502+
a.setAttachmentId(new UUID(bb.getLong(), bb.getLong()));
503+
} else {
504+
String s = rs.getString("attachment_id");
505+
if (s != null && !s.isEmpty()) {
506+
a.setAttachmentId(UUID.fromString(s));
507+
}
508+
}
509+
a.setFileName(rs.getString("file_name"));
510+
a.setAttributeName(rs.getString("attribute_name"));
511+
a.setDescription(rs.getString("description"));
512+
long size = rs.getLong("attachment_size");
513+
if (!rs.wasNull()) {
514+
a.setAttachmentSize(size);
515+
}
516+
a.setMimeType(rs.getString("mime_type"));
517+
java.sql.Timestamp ts = rs.getTimestamp("created_at");
518+
if (ts != null) {
519+
a.setCreatedAt(OffsetDateTime.ofInstant(ts.toInstant(), ZoneId.of("UTC")));
520+
}
521+
a.setCreatedBy(rs.getString("created_by"));
522+
attachments.add(a);
523+
}
524+
}
525+
}
526+
} finally {
527+
if (ds != null) {
528+
ds.dispose();
529+
}
530+
}
531+
return attachments;
532+
}
533+
534+
public static AttachmentWithBinary getAttachment(TMFeatureType featureType, UUID attachmentId)
535+
throws IOException, SQLException {
536+
537+
String querySql = MessageFormat.format(
538+
"SELECT attachment, attachment_size, mime_type, file_name FROM {0}_attachments WHERE attachment_id = ?",
539+
featureType.getName());
540+
JDBCDataStore ds = null;
541+
try {
542+
byte[] attachment;
543+
ds = (JDBCDataStore) new JDBCFeatureSourceHelper().createDataStore(featureType.getFeatureSource());
544+
try (Connection conn = ds.getDataSource().getConnection();
545+
PreparedStatement stmt = conn.prepareStatement(querySql)) {
546+
547+
if (featureType
548+
.getFeatureSource()
549+
.getJdbcConnection()
550+
.getDbtype()
551+
.equals(JDBCConnectionProperties.DbtypeEnum.ORACLE)) {
552+
stmt.setBytes(1, asBytes(attachmentId));
553+
} else {
554+
stmt.setObject(1, attachmentId);
555+
}
556+
557+
try (ResultSet rs = stmt.executeQuery()) {
558+
if (rs.next()) {
559+
attachment = rs.getBytes("attachment");
560+
AttachmentMetadata a = new AttachmentMetadata();
561+
long size = rs.getLong("attachment_size");
562+
if (!rs.wasNull()) {
563+
a.setAttachmentSize(size);
564+
}
565+
a.setMimeType(rs.getString("mime_type"));
566+
a.setFileName(rs.getString("file_name"));
567+
return new AttachmentWithBinary(
568+
a, ByteBuffer.wrap(attachment).asReadOnlyBuffer());
569+
} else {
570+
return null;
571+
}
572+
}
573+
}
574+
} finally {
575+
if (ds != null) {
576+
ds.dispose();
577+
}
578+
}
579+
}
580+
581+
public record AttachmentWithBinary(
582+
@NotNull AttachmentMetadata attachmentMetadata, @NotNull ByteBuffer attachment) {}
429583
}

0 commit comments

Comments
 (0)