1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19 package org.mycore.restapi.v2;
20
21 import static org.mycore.common.MCRConstants.XSI_NAMESPACE;
22 import static org.mycore.restapi.v2.MCRRestAuthorizationFilter.PARAM_MCRID;
23
24 import java.awt.image.BufferedImage;
25 import java.io.FileNotFoundException;
26 import java.io.IOException;
27 import java.io.InputStream;
28 import java.io.UncheckedIOException;
29 import java.lang.annotation.Annotation;
30 import java.net.URI;
31 import java.nio.file.Files;
32 import java.nio.file.attribute.FileTime;
33 import java.util.Collection;
34 import java.util.Collections;
35 import java.util.Date;
36 import java.util.List;
37 import java.util.Map;
38 import java.util.Objects;
39 import java.util.Optional;
40 import java.util.concurrent.TimeUnit;
41 import java.util.function.Predicate;
42 import java.util.stream.Collectors;
43
44 import jakarta.ws.rs.DefaultValue;
45 import jakarta.ws.rs.QueryParam;
46 import jakarta.ws.rs.core.UriBuilder;
47 import org.apache.commons.lang3.reflect.TypeUtils;
48 import org.apache.logging.log4j.LogManager;
49 import org.apache.logging.log4j.Logger;
50 import org.jdom2.Document;
51 import org.jdom2.Element;
52 import org.jdom2.JDOMException;
53 import org.mycore.access.MCRAccessException;
54 import org.mycore.access.MCRAccessManager;
55 import org.mycore.common.MCRCoreVersion;
56 import org.mycore.common.MCRException;
57 import org.mycore.common.MCRPersistenceException;
58 import org.mycore.common.config.MCRConfiguration2;
59 import org.mycore.common.content.MCRContent;
60 import org.mycore.common.content.MCRJDOMContent;
61 import org.mycore.common.content.MCRStreamContent;
62 import org.mycore.common.content.MCRStringContent;
63 import org.mycore.common.xml.MCRXMLParserFactory;
64 import org.mycore.datamodel.classifications2.MCRCategoryDAOFactory;
65 import org.mycore.datamodel.classifications2.MCRCategoryID;
66 import org.mycore.datamodel.common.MCRAbstractMetadataVersion;
67 import org.mycore.datamodel.common.MCRActiveLinkException;
68 import org.mycore.datamodel.common.MCRObjectIDDate;
69 import org.mycore.datamodel.common.MCRXMLMetadataManager;
70 import org.mycore.datamodel.metadata.MCRMetadataManager;
71 import org.mycore.datamodel.metadata.MCRObject;
72 import org.mycore.datamodel.metadata.MCRObjectID;
73 import org.mycore.datamodel.niofs.MCRPath;
74 import org.mycore.datamodel.objectinfo.MCRObjectQuery;
75 import org.mycore.datamodel.objectinfo.MCRObjectQueryResolver;
76 import org.mycore.frontend.jersey.MCRCacheControl;
77 import org.mycore.media.services.MCRThumbnailGenerator;
78 import org.mycore.restapi.annotations.MCRAccessControlExposeHeaders;
79 import org.mycore.restapi.annotations.MCRApiDraft;
80 import org.mycore.restapi.annotations.MCRParam;
81 import org.mycore.restapi.annotations.MCRParams;
82 import org.mycore.restapi.annotations.MCRRequireTransaction;
83 import org.mycore.restapi.converter.MCRContentAbstractWriter;
84 import org.mycore.restapi.v2.model.MCRRestObjectIDDate;
85 import org.xml.sax.SAXException;
86 import org.xml.sax.SAXParseException;
87
88 import com.fasterxml.jackson.databind.SerializationFeature;
89
90 import com.fasterxml.jackson.jakarta.rs.annotation.JacksonFeatures;
91 import io.swagger.v3.oas.annotations.OpenAPIDefinition;
92 import io.swagger.v3.oas.annotations.Operation;
93 import io.swagger.v3.oas.annotations.Parameter;
94 import io.swagger.v3.oas.annotations.headers.Header;
95 import io.swagger.v3.oas.annotations.media.ArraySchema;
96 import io.swagger.v3.oas.annotations.media.Content;
97 import io.swagger.v3.oas.annotations.media.ExampleObject;
98 import io.swagger.v3.oas.annotations.media.Schema;
99 import io.swagger.v3.oas.annotations.parameters.RequestBody;
100 import io.swagger.v3.oas.annotations.responses.ApiResponse;
101 import io.swagger.v3.oas.annotations.tags.Tag;
102 import jakarta.servlet.ServletContext;
103 import jakarta.ws.rs.BadRequestException;
104 import jakarta.ws.rs.Consumes;
105 import jakarta.ws.rs.DELETE;
106 import jakarta.ws.rs.ForbiddenException;
107 import jakarta.ws.rs.GET;
108 import jakarta.ws.rs.InternalServerErrorException;
109 import jakarta.ws.rs.NotFoundException;
110 import jakarta.ws.rs.POST;
111 import jakarta.ws.rs.PUT;
112 import jakarta.ws.rs.Path;
113 import jakarta.ws.rs.PathParam;
114 import jakarta.ws.rs.Produces;
115 import jakarta.ws.rs.core.Context;
116 import jakarta.ws.rs.core.GenericEntity;
117 import jakarta.ws.rs.core.HttpHeaders;
118 import jakarta.ws.rs.core.MediaType;
119 import jakarta.ws.rs.core.Request;
120 import jakarta.ws.rs.core.Response;
121 import jakarta.ws.rs.core.UriInfo;
122 import jakarta.xml.bind.annotation.XmlElementWrapper;
123
124 @Path("/objects")
125 @OpenAPIDefinition(tags = {
126 @Tag(name = MCRRestUtils.TAG_MYCORE_OBJECT, description = "Operations on metadata objects"),
127 @Tag(name = MCRRestUtils.TAG_MYCORE_DERIVATE,
128 description = "Operations on derivates belonging to metadata objects"),
129 @Tag(name = MCRRestUtils.TAG_MYCORE_FILE, description = "Operations on files in derivates"),
130 })
131 public class MCRRestObjects {
132
133 public static final String PARAM_AFTER_ID = "after_id";
134
135 public static final String PARAM_OFFSET = "offset";
136
137 public static final String PARAM_LIMIT = "limit";
138
139 public static final String PARAM_TYPE = "type";
140
141 public static final String PARAM_PROJECT = "project";
142
143 public static final String PARAM_NUMBER_GREATER = "number_greater";
144
145 public static final String PARAM_NUMBER_LESS = "number_less";
146
147 public static final String PARAM_CREATED_AFTER = "created_after";
148
149 public static final String PARAM_CREATED_BEFORE = "created_before";
150
151 public static final String PARAM_MODIFIED_AFTER = "modified_after";
152
153 public static final String PARAM_MODIFIED_BEFORE = "modified_before";
154
155 public static final String PARAM_DELETED_AFTER = "deleted_after";
156
157 public static final String PARAM_DELETED_BEFORE = "deleted_before";
158
159 public static final String PARAM_CREATED_BY = "created_by";
160
161 public static final String PARAM_MODIFIED_BY = "modified_by";
162
163 public static final String PARAM_DELETED_BY = "deleted_by";
164
165 public static final String PARAM_SORT_ORDER = "sort_order";
166
167 public static final String PARAM_SORT_BY = "sort_by";
168
169 public static final List<MCRThumbnailGenerator> THUMBNAIL_GENERATORS = Collections
170 .unmodifiableList(MCRConfiguration2
171 .getOrThrow("MCR.Media.Thumbnail.Generators", MCRConfiguration2::splitValue)
172 .map(MCRConfiguration2::instantiateClass)
173 .map(MCRThumbnailGenerator.class::cast)
174 .collect(Collectors.toList()));
175
176 private static final String PARAM_CATEGORIES = "category";
177
178 private static final Logger LOGGER = LogManager.getLogger();
179
180 private static final int PAGE_SIZE_MAX = MCRConfiguration2.getInt("MCR.RestAPI.V2.ListObjects.PageSize.Max")
181 .orElseThrow();
182
183 private static final int PAGE_SIZE_DEFAULT = MCRConfiguration2
184 .getInt("MCR.RestAPI.V2.ListObjects.PageSize.Default")
185 .orElse(1000);
186
187 @Context
188 Request request;
189
190 @Context
191 ServletContext context;
192
193 @Context
194 UriInfo uriInfo;
195
196 @GET
197 @Produces({ MediaType.APPLICATION_XML, MediaType.APPLICATION_JSON + ";charset=UTF-8" })
198 @MCRCacheControl(maxAge = @MCRCacheControl.Age(time = 1, unit = TimeUnit.HOURS),
199 sMaxAge = @MCRCacheControl.Age(time = 1, unit = TimeUnit.HOURS))
200 @Operation(
201 summary = "Lists all objects in this repository",
202 parameters = {
203 @Parameter(
204 name = PARAM_AFTER_ID,
205 description = "the id after which the results should be listed. Do not use after_id and offset " +
206 "together."),
207 @Parameter(
208 name = PARAM_OFFSET,
209 description = "dictates the number of rows to skip from the beginning of the returned data before " +
210 "presenting the results. Do not use after_id and offset together."),
211 @Parameter(
212 name = PARAM_LIMIT,
213 description = "limits the number of result returned"),
214 @Parameter(
215 name = PARAM_TYPE,
216 description = "objects with have the type in the id"),
217 @Parameter(
218 name = PARAM_PROJECT,
219 description = "only objects that have the project in the id"),
220 @Parameter(
221 name = PARAM_NUMBER_GREATER,
222 description = "only objects which id have a number greater than this"),
223 @Parameter(
224 name = PARAM_NUMBER_LESS,
225 description = "only objects which id have a number less than this"),
226 @Parameter(
227 name = PARAM_CREATED_AFTER,
228 description = "objects created after this date"),
229 @Parameter(
230 name = PARAM_CREATED_BEFORE,
231 description = "objects created before this date"),
232 @Parameter(
233 name = PARAM_MODIFIED_AFTER,
234 description = "objects last modified after this date"),
235 @Parameter(
236 name = PARAM_MODIFIED_BEFORE,
237 description = "objects last modified before this date"),
238 @Parameter(
239 name = PARAM_MODIFIED_AFTER,
240 description = "objects last modified after this date"),
241 @Parameter(
242 name = PARAM_MODIFIED_BEFORE,
243 description = "objects last modified before this date"),
244 @Parameter(
245 name = PARAM_SORT_ORDER,
246 description = "sort results 'asc' or 'desc'"),
247 @Parameter(
248 name = PARAM_SORT_BY,
249 description = "sort objects by 'id', 'created' (default) or 'modified'")
250 },
251 responses = @ApiResponse(
252 content = @Content(array = @ArraySchema(schema = @Schema(implementation = MCRObjectIDDate.class)))),
253 tags = MCRRestUtils.TAG_MYCORE_OBJECT)
254 @XmlElementWrapper(name = "mycoreobjects")
255 @JacksonFeatures(serializationDisable = { SerializationFeature.WRITE_DATES_AS_TIMESTAMPS })
256 public Response listObjects(
257 @QueryParam(PARAM_AFTER_ID) MCRObjectID afterID,
258 @QueryParam(PARAM_OFFSET) Integer offset,
259 @QueryParam(PARAM_LIMIT) Integer limit,
260 @QueryParam(PARAM_TYPE) String type,
261 @QueryParam(PARAM_PROJECT) String project,
262 @QueryParam(PARAM_NUMBER_GREATER) Integer numberGreater,
263 @QueryParam(PARAM_NUMBER_LESS) Integer numberLess,
264 @QueryParam(PARAM_CREATED_AFTER) Date createdAfter,
265 @QueryParam(PARAM_CREATED_BEFORE) Date createdBefore,
266 @QueryParam(PARAM_MODIFIED_AFTER) Date modifiedAfter,
267 @QueryParam(PARAM_MODIFIED_BEFORE) Date modifiedBefore,
268 @QueryParam(PARAM_DELETED_AFTER) Date deletedAfter,
269 @QueryParam(PARAM_DELETED_BEFORE) Date deletedBefore,
270 @QueryParam(PARAM_CREATED_BY) String createdBy,
271 @QueryParam(PARAM_MODIFIED_BY) String modifiedBy,
272 @QueryParam(PARAM_DELETED_BY) String deletedBy,
273 @QueryParam(PARAM_SORT_BY) @DefaultValue("id") MCRObjectQuery.SortBy sortBy,
274 @QueryParam(PARAM_SORT_ORDER) @DefaultValue("asc") MCRObjectQuery.SortOrder sortOrder,
275 @QueryParam(PARAM_CATEGORIES) List<String> categories) throws IOException {
276
277 MCRObjectQuery query = new MCRObjectQuery();
278 int limitInt = Optional.ofNullable(limit)
279 .map(l -> Integer.min(l, PAGE_SIZE_MAX))
280 .orElse(PAGE_SIZE_DEFAULT);
281
282 query.limit(limitInt);
283
284 Optional.ofNullable(offset).ifPresent(query::offset);
285 Optional.ofNullable(afterID).ifPresent(query::afterId);
286
287 Optional.ofNullable(type).ifPresent(query::type);
288 Optional.ofNullable(project).ifPresent(query::project);
289
290 Optional.ofNullable(modifiedAfter).map(Date::toInstant).ifPresent(query::modifiedAfter);
291 Optional.ofNullable(modifiedBefore).map(Date::toInstant).ifPresent(query::modifiedBefore);
292
293 Optional.ofNullable(createdAfter).map(Date::toInstant).ifPresent(query::createdAfter);
294 Optional.ofNullable(createdBefore).map(Date::toInstant).ifPresent(query::createdBefore);
295
296 Optional.ofNullable(deletedAfter).map(Date::toInstant).ifPresent(query::deletedAfter);
297 Optional.ofNullable(deletedBefore).map(Date::toInstant).ifPresent(query::deletedBefore);
298
299 Optional.ofNullable(createdBy).ifPresent(query::createdBy);
300 Optional.ofNullable(modifiedBy).ifPresent(query::modifiedBy);
301 Optional.ofNullable(deletedBy).ifPresent(query::deletedBy);
302
303 Optional.ofNullable(numberGreater).ifPresent(query::numberGreater);
304 Optional.ofNullable(numberLess).ifPresent(query::numberLess);
305
306 List<String> includeCategories = query.getIncludeCategories();
307 categories.stream()
308 .filter(Predicate.not(String::isBlank))
309 .forEach(includeCategories::add);
310
311 query.sort(sortBy, sortOrder);
312
313 MCRObjectQueryResolver queryResolver = MCRObjectQueryResolver.getInstance();
314
315 List<MCRObjectIDDate> idDates = limitInt == 0 ? Collections.emptyList() : queryResolver.getIdDates(query);
316
317 List<MCRRestObjectIDDate> restIdDate = idDates.stream().map(MCRRestObjectIDDate::new)
318 .collect(Collectors.toList());
319
320 int count = queryResolver.count(query);
321 UriBuilder nextBuilder = null;
322 if (query.afterId() != null && idDates.size() == limitInt) {
323 nextBuilder = uriInfo.getRequestUriBuilder();
324 nextBuilder.replaceQueryParam(PARAM_AFTER_ID, idDates.get(idDates.size() - 1).getId());
325 } else {
326 if (query.offset() + query.limit() < count) {
327 nextBuilder = uriInfo.getRequestUriBuilder();
328 nextBuilder.replaceQueryParam(PARAM_OFFSET, String.valueOf(query.offset() + limitInt));
329 }
330 }
331
332 Response.ResponseBuilder responseBuilder = Response.ok(new GenericEntity<>(restIdDate) {
333 })
334 .header("X-Total-Count", count);
335
336 if (nextBuilder != null) {
337 responseBuilder.link("next", nextBuilder.toString());
338 }
339
340 return responseBuilder.build();
341 }
342
343 @POST
344 @Operation(
345 summary = "Create a new MyCoRe Object",
346 responses = @ApiResponse(responseCode = "201",
347 headers = @Header(name = HttpHeaders.LOCATION, description = "URL of the new MyCoRe Object")),
348 tags = MCRRestUtils.TAG_MYCORE_OBJECT)
349 @Consumes(MediaType.APPLICATION_XML)
350 @RequestBody(required = true,
351 content = @Content(mediaType = MediaType.APPLICATION_XML))
352 @MCRRequireTransaction
353 @MCRAccessControlExposeHeaders(HttpHeaders.LOCATION)
354 public Response createObject(String xml) {
355 try {
356 Document doc = MCRXMLParserFactory.getNonValidatingParser().parseXML(new MCRStringContent(xml));
357 Element eMCRObj = doc.getRootElement();
358 if (eMCRObj.getAttributeValue("ID") != null) {
359 MCRObjectID id = MCRObjectID.getInstance(eMCRObj.getAttributeValue("ID"));
360 MCRObjectID newID = MCRObjectID.getNextFreeId(id.getBase());
361 eMCRObj.setAttribute("ID", newID.toString());
362 if (eMCRObj.getAttribute("label") == null) {
363 eMCRObj.setAttribute("label", newID.toString());
364 }
365 if (eMCRObj.getAttribute("version") == null) {
366 eMCRObj.setAttribute("version", MCRCoreVersion.getVersion());
367 }
368 eMCRObj.setAttribute("noNamespaceSchemaLocation", "datamodel-" + newID.getTypeId() + ".xsd",
369 XSI_NAMESPACE);
370 } else {
371
372 throw new BadRequestException("Please provide an object with ID");
373 }
374
375 MCRObject mcrObj = new MCRObject(new MCRJDOMContent(doc).asByteArray(), true);
376 LOGGER.debug("Create new MyCoRe Object");
377 MCRMetadataManager.create(mcrObj);
378 return Response.created(uriInfo.getAbsolutePathBuilder().path(mcrObj.getId().toString()).build()).build();
379 } catch (MCRPersistenceException | SAXParseException | IOException e) {
380 throw new InternalServerErrorException(e);
381 } catch (MCRAccessException e) {
382 throw new ForbiddenException(e);
383 }
384 }
385
386 @GET
387 @Produces({ MediaType.APPLICATION_XML, MediaType.APPLICATION_JSON + ";charset=UTF-8" })
388 @MCRCacheControl(maxAge = @MCRCacheControl.Age(time = 1, unit = TimeUnit.DAYS),
389 sMaxAge = @MCRCacheControl.Age(time = 1, unit = TimeUnit.DAYS))
390 @Path("/{" + PARAM_MCRID + "}")
391 @Operation(
392 summary = "Returns MCRObject with the given " + PARAM_MCRID + ".",
393 tags = MCRRestUtils.TAG_MYCORE_OBJECT)
394 public Response getObject(@Parameter(example = "mir_mods_00004711") @PathParam(PARAM_MCRID) MCRObjectID id)
395 throws IOException {
396 long modified = MCRXMLMetadataManager.instance().getLastModified(id);
397 if (modified < 0) {
398 throw new NotFoundException("MCRObject " + id + " not found");
399 }
400 Date lastModified = new Date(modified);
401 Optional<Response> cachedResponse = MCRRestUtils.getCachedResponse(request, lastModified);
402 if (cachedResponse.isPresent()) {
403 return cachedResponse.get();
404 }
405 MCRContent mcrContent = MCRXMLMetadataManager.instance().retrieveContent(id);
406 return Response.ok()
407 .entity(mcrContent,
408 new Annotation[] { MCRParams.Factory
409 .get(MCRParam.Factory.get(MCRContentAbstractWriter.PARAM_OBJECTTYPE, id.getTypeId())) })
410 .lastModified(lastModified)
411 .build();
412 }
413
414 @GET
415 @Produces({ MediaType.APPLICATION_XML })
416 @MCRCacheControl(maxAge = @MCRCacheControl.Age(time = 1, unit = TimeUnit.DAYS),
417 sMaxAge = @MCRCacheControl.Age(time = 1, unit = TimeUnit.DAYS))
418 @Path("/{" + PARAM_MCRID + "}/metadata")
419 @Operation(
420 summary = "Returns metadata section MCRObject with the given " + PARAM_MCRID + ".",
421 tags = MCRRestUtils.TAG_MYCORE_OBJECT)
422 public Response getObjectMetadata(@Parameter(example = "mir_mods_00004711") @PathParam(PARAM_MCRID) MCRObjectID id)
423 throws IOException {
424 long modified = MCRXMLMetadataManager.instance().getLastModified(id);
425 if (modified < 0) {
426 throw new NotFoundException("MCRObject " + id + " not found");
427 }
428 Date lastModified = new Date(modified);
429 Optional<Response> cachedResponse = MCRRestUtils.getCachedResponse(request, lastModified);
430 if (cachedResponse.isPresent()) {
431 return cachedResponse.get();
432 }
433 MCRObject mcrObj = MCRMetadataManager.retrieveMCRObject(id);
434 MCRContent mcrContent = new MCRJDOMContent(mcrObj.getMetadata().createXML());
435 return Response.ok()
436 .entity(mcrContent,
437 new Annotation[] { MCRParams.Factory
438 .get(MCRParam.Factory.get(MCRContentAbstractWriter.PARAM_OBJECTTYPE, id.getTypeId())) })
439 .lastModified(lastModified)
440 .build();
441 }
442
443 @GET
444 @Path("{" + PARAM_MCRID + "}/thumb-{size}.{ext : (jpg|jpeg|png)}")
445 @Produces({ "image/png", "image/jpeg" })
446 @Operation(
447 summary = "Returns thumbnail of MCRObject with the given " + PARAM_MCRID + ".",
448 tags = MCRRestUtils.TAG_MYCORE_OBJECT)
449 @MCRCacheControl(maxAge = @MCRCacheControl.Age(time = 1, unit = TimeUnit.DAYS),
450 sMaxAge = @MCRCacheControl.Age(time = 1, unit = TimeUnit.DAYS))
451 public Response getThumbnailWithSize(@PathParam(PARAM_MCRID) String id, @PathParam("size") int size,
452 @PathParam("ext") String ext) {
453 return getThumbnail(id, size, ext);
454 }
455
456 @GET
457 @Path("{" + PARAM_MCRID + "}/thumb.{ext : (jpg|jpeg|png)}")
458 @Produces({ "image/png", "image/jpeg" })
459 @Operation(
460 summary = "Returns thumbnail of MCRObject with the given " + PARAM_MCRID + ".",
461 tags = MCRRestUtils.TAG_MYCORE_OBJECT)
462 @MCRCacheControl(maxAge = @MCRCacheControl.Age(time = 1, unit = TimeUnit.DAYS),
463 sMaxAge = @MCRCacheControl.Age(time = 1, unit = TimeUnit.DAYS))
464 public Response getThumbnail(@PathParam(PARAM_MCRID) String id, @PathParam("ext") String ext) {
465 int defaultSize = MCRConfiguration2.getOrThrow("MCR.Media.Thumbnail.DefaultSize", Integer::parseInt);
466 return getThumbnail(id, defaultSize, ext);
467 }
468
469 private Response getThumbnail(String id, int size, String ext) {
470 List<MCRPath> mainDocs = MCRMetadataManager.getDerivateIds(MCRObjectID.getInstance(id), 1, TimeUnit.MINUTES)
471 .stream()
472 .filter(d -> MCRAccessManager.checkDerivateContentPermission(d, MCRAccessManager.PERMISSION_READ))
473 .map(d -> {
474 String nameOfMainFile = MCRMetadataManager.retrieveMCRDerivate(d).getDerivate().getInternals()
475 .getMainDoc();
476 return nameOfMainFile.isEmpty() ? null : MCRPath.getPath(d.toString(), '/' + nameOfMainFile);
477 })
478 .filter(Objects::nonNull)
479 .collect(Collectors.toList());
480
481 if (mainDocs.isEmpty()) {
482 throw new NotFoundException();
483 }
484
485 for (MCRPath mainDoc : mainDocs) {
486 Optional<MCRThumbnailGenerator> thumbnailGenerator = THUMBNAIL_GENERATORS.stream()
487 .filter(g -> g.matchesFileType(context.getMimeType(mainDoc.getFileName().toString()), mainDoc))
488 .findFirst();
489 if (thumbnailGenerator.isPresent()) {
490 try {
491 FileTime lastModified = Files.getLastModifiedTime(mainDoc);
492 Date lastModifiedDate = new Date(lastModified.toMillis());
493 Optional<Response> cachedResponse = MCRRestUtils.getCachedResponse(request, lastModifiedDate);
494 return cachedResponse.orElseGet(() -> {
495 Optional<BufferedImage> thumbnail = null;
496 try {
497 thumbnail = thumbnailGenerator.get().getThumbnail(mainDoc, size);
498 } catch (IOException e) {
499 throw new UncheckedIOException(e);
500 }
501 return thumbnail.map(b -> {
502 String type = "image/png";
503 if ("jpg".equals(ext) || "jpeg".equals(ext)) {
504 type = "image/jpeg";
505 }
506 return Response.ok(b)
507 .lastModified(lastModifiedDate)
508 .type(type)
509 .build();
510 }).orElseGet(() -> Response.status(Response.Status.NOT_FOUND).build());
511 });
512 } catch (FileNotFoundException e) {
513 continue;
514 } catch (IOException e) {
515 throw new InternalServerErrorException(e);
516 } catch (UncheckedIOException e) {
517 throw new InternalServerErrorException(e.getCause());
518 }
519 }
520 }
521
522 throw new NotFoundException();
523 }
524
525 @GET
526 @Produces({ MediaType.APPLICATION_XML, MediaType.APPLICATION_JSON + ";charset=UTF-8" })
527 @MCRCacheControl(maxAge = @MCRCacheControl.Age(time = 1, unit = TimeUnit.DAYS),
528 sMaxAge = @MCRCacheControl.Age(time = 1, unit = TimeUnit.DAYS))
529 @Path("/{" + PARAM_MCRID + "}/versions")
530 @Operation(
531 summary = "Returns MCRObject with the given " + PARAM_MCRID + ".",
532 responses = @ApiResponse(content = @Content(
533 array = @ArraySchema(uniqueItems = true,
534 schema = @Schema(implementation = MCRAbstractMetadataVersion.class)))),
535 tags = MCRRestUtils.TAG_MYCORE_OBJECT)
536 @JacksonFeatures(serializationDisable = { SerializationFeature.WRITE_DATES_AS_TIMESTAMPS })
537 @XmlElementWrapper(name = "versions")
538 public Response getObjectVersions(@Parameter(example = "mir_mods_00004711") @PathParam(PARAM_MCRID) MCRObjectID id)
539 throws IOException {
540 long modified = MCRXMLMetadataManager.instance().getLastModified(id);
541 if (modified < 0) {
542 throw MCRErrorResponse.fromStatus(Response.Status.NOT_FOUND.getStatusCode())
543 .withErrorCode(MCRErrorCodeConstants.MCROBJECT_NOT_FOUND)
544 .withMessage("MCRObject " + id + " not found")
545 .toException();
546 }
547 Date lastModified = new Date(modified);
548 Optional<Response> cachedResponse = MCRRestUtils.getCachedResponse(request, lastModified);
549 if (cachedResponse.isPresent()) {
550 return cachedResponse.get();
551 }
552 List<? extends MCRAbstractMetadataVersion<?>> versions = MCRXMLMetadataManager.instance().listRevisions(id);
553 return Response.ok()
554 .entity(new GenericEntity<>(versions, TypeUtils.parameterize(List.class, MCRAbstractMetadataVersion.class)))
555 .lastModified(lastModified)
556 .build();
557 }
558
559 @GET
560 @Produces({ MediaType.APPLICATION_XML, MediaType.APPLICATION_JSON + ";charset=UTF-8" })
561 @MCRCacheControl(maxAge = @MCRCacheControl.Age(time = 1000, unit = TimeUnit.DAYS),
562 sMaxAge = @MCRCacheControl.Age(time = 1000, unit = TimeUnit.DAYS))
563 @Path("/{" + PARAM_MCRID + "}/versions/{revision}")
564 @Operation(
565 summary = "Returns MCRObject with the given " + PARAM_MCRID + " and revision.",
566 tags = MCRRestUtils.TAG_MYCORE_OBJECT)
567 public Response getObjectVersion(@Parameter(example = "mir_mods_00004711") @PathParam(PARAM_MCRID) MCRObjectID id,
568 @PathParam("revision") String revision)
569 throws IOException {
570 MCRContent mcrContent = MCRXMLMetadataManager.instance().retrieveContent(id, revision);
571 if (mcrContent == null) {
572 throw MCRErrorResponse.fromStatus(Response.Status.NOT_FOUND.getStatusCode())
573 .withErrorCode(MCRErrorCodeConstants.MCROBJECT_REVISION_NOT_FOUND)
574 .withMessage("MCRObject " + id + " has no revision " + revision + ".")
575 .toException();
576 }
577 long modified = mcrContent.lastModified();
578 Date lastModified = new Date(modified);
579 Optional<Response> cachedResponse = MCRRestUtils.getCachedResponse(request, lastModified);
580 if (cachedResponse.isPresent()) {
581 return cachedResponse.get();
582 }
583 LogManager.getLogger().info("OK: {}", mcrContent.getETag());
584 return Response.ok()
585 .entity(mcrContent,
586 new Annotation[] { MCRParams.Factory
587 .get(MCRParam.Factory.get(MCRContentAbstractWriter.PARAM_OBJECTTYPE, id.getTypeId())) })
588 .lastModified(lastModified)
589 .build();
590 }
591
592 @PUT
593 @Consumes(MediaType.APPLICATION_XML)
594 @Path("/{" + PARAM_MCRID + "}")
595 @Operation(summary = "Creates or updates MCRObject with the body of this request",
596 tags = MCRRestUtils.TAG_MYCORE_OBJECT,
597 responses = {
598 @ApiResponse(responseCode = "400",
599 content = { @Content(mediaType = MediaType.TEXT_PLAIN) },
600 description = "'Invalid body content' or 'MCRObjectID mismatch'"),
601 @ApiResponse(responseCode = "201", description = "MCRObject successfully created"),
602 @ApiResponse(responseCode = "204", description = "MCRObject successfully updated"),
603 })
604 @MCRRequireTransaction
605 public Response updateObject(@PathParam(PARAM_MCRID) MCRObjectID id,
606 @Parameter(required = true,
607 description = "MCRObject XML",
608 examples = @ExampleObject("<mycoreobject ID=\"{mcrid}\" ..>\n...\n</mycorobject>")) InputStream xmlSource)
609 throws IOException {
610
611 try {
612 long lastModified = MCRXMLMetadataManager.instance().getLastModified(id);
613 if (lastModified >= 0) {
614 Date lmDate = new Date(lastModified);
615 Optional<Response> cachedResponse = MCRRestUtils.getCachedResponse(request, lmDate);
616 if (cachedResponse.isPresent()) {
617 return cachedResponse.get();
618 }
619 }
620 } catch (Exception e) {
621
622 }
623 MCRStreamContent inputContent = new MCRStreamContent(xmlSource, null, MCRObject.ROOT_NAME);
624 MCRObject updatedObject;
625 try {
626 updatedObject = new MCRObject(inputContent.asXML());
627 updatedObject.validate();
628 } catch (JDOMException | SAXException | MCRException e) {
629 throw MCRErrorResponse.fromStatus(Response.Status.BAD_REQUEST.getStatusCode())
630 .withErrorCode(MCRErrorCodeConstants.MCROBJECT_INVALID)
631 .withMessage("MCRObject " + id + " is not valid")
632 .withDetail(e.getMessage())
633 .withCause(e)
634 .toException();
635 }
636 if (!id.equals(updatedObject.getId())) {
637 throw MCRErrorResponse.fromStatus(Response.Status.BAD_REQUEST.getStatusCode())
638 .withErrorCode(MCRErrorCodeConstants.MCROBJECT_ID_MISMATCH)
639 .withMessage("MCRObject " + id + " cannot be overwritten by " + updatedObject.getId() + ".")
640 .toException();
641 }
642 try {
643 if (MCRMetadataManager.exists(id)) {
644 MCRMetadataManager.update(updatedObject);
645 return Response.status(Response.Status.NO_CONTENT).build();
646 } else {
647 MCRMetadataManager.create(updatedObject);
648 return Response.status(Response.Status.CREATED).build();
649 }
650 } catch (MCRAccessException e) {
651 throw MCRErrorResponse.fromStatus(Response.Status.FORBIDDEN.getStatusCode())
652 .withErrorCode(MCRErrorCodeConstants.MCROBJECT_NO_PERMISSION)
653 .withMessage("You may not modify or create MCRObject " + id + ".")
654 .withDetail(e.getMessage())
655 .withCause(e)
656 .toException();
657 }
658 }
659
660 @PUT
661 @Consumes(MediaType.APPLICATION_XML)
662 @Path("/{" + PARAM_MCRID + "}/metadata")
663 @Operation(summary = "Updates the metadata section of a MCRObject with the body of this request",
664 tags = MCRRestUtils.TAG_MYCORE_OBJECT,
665 responses = {
666 @ApiResponse(responseCode = "400",
667 content = { @Content(mediaType = MediaType.TEXT_PLAIN) },
668 description = "'Invalid body content' or 'MCRObjectID mismatch'"),
669 @ApiResponse(responseCode = "204", description = "MCRObject metadata successfully updated"),
670 })
671 @MCRRequireTransaction
672 public Response updateObjectMetadata(@PathParam(PARAM_MCRID) MCRObjectID id,
673 @Parameter(required = true,
674 description = "MCRObject XML",
675 examples = @ExampleObject("<metadata>\n...\n</metadata>")) InputStream xmlSource)
676 throws IOException {
677
678 try {
679 long lastModified = MCRXMLMetadataManager.instance().getLastModified(id);
680 if (lastModified >= 0) {
681 Date lmDate = new Date(lastModified);
682 Optional<Response> cachedResponse = MCRRestUtils.getCachedResponse(request, lmDate);
683 if (cachedResponse.isPresent()) {
684 return cachedResponse.get();
685 }
686 }
687 } catch (Exception e) {
688
689 }
690 MCRStreamContent inputContent = new MCRStreamContent(xmlSource, null);
691 MCRObject updatedObject;
692 try {
693 updatedObject = MCRMetadataManager.retrieveMCRObject(id);
694 updatedObject.getMetadata().setFromDOM(inputContent.asXML().getRootElement().detach());
695 updatedObject.validate();
696 } catch (JDOMException | SAXException | MCRException e) {
697 throw MCRErrorResponse.fromStatus(Response.Status.BAD_REQUEST.getStatusCode())
698 .withErrorCode(MCRErrorCodeConstants.MCROBJECT_INVALID)
699 .withMessage("MCRObject " + id + " is not valid")
700 .withDetail(e.getMessage())
701 .withCause(e)
702 .toException();
703 }
704 try {
705 MCRMetadataManager.update(updatedObject);
706 return Response.status(Response.Status.NO_CONTENT).build();
707 } catch (MCRAccessException e) {
708 throw MCRErrorResponse.fromStatus(Response.Status.FORBIDDEN.getStatusCode())
709 .withErrorCode(MCRErrorCodeConstants.MCROBJECT_NO_PERMISSION)
710 .withMessage("You may not modify or create metadata of MCRObject " + id + ".")
711 .withDetail(e.getMessage())
712 .withCause(e)
713 .toException();
714 }
715 }
716
717 @DELETE
718 @Path("/{" + PARAM_MCRID + "}")
719 @Operation(summary = "Deletes MCRObject {" + PARAM_MCRID + "}",
720 tags = MCRRestUtils.TAG_MYCORE_OBJECT,
721 responses = {
722 @ApiResponse(responseCode = "204", description = "MCRObject successfully deleted"),
723 @ApiResponse(responseCode = "409",
724 description = "MCRObject could not be deleted as it is referenced.",
725 content = @Content(schema = @Schema(
726 description = "Map<String, <Collection<String>> of source (key) to targets (value)",
727 implementation = Map.class))),
728 })
729 @MCRRequireTransaction
730 public Response deleteObject(@PathParam(PARAM_MCRID) MCRObjectID id) {
731
732 if (!MCRMetadataManager.exists(id)) {
733 throw MCRErrorResponse.fromStatus(Response.Status.NOT_FOUND.getStatusCode())
734 .withErrorCode(MCRErrorCodeConstants.MCROBJECT_NOT_FOUND)
735 .withMessage("MCRObject " + id + " not found")
736 .toException();
737 }
738 try {
739 MCRMetadataManager.deleteMCRObject(id);
740 } catch (MCRActiveLinkException e) {
741 Map<String, Collection<String>> activeLinks = e.getActiveLinks();
742 throw MCRErrorResponse.fromStatus(Response.Status.CONFLICT.getStatusCode())
743 .withErrorCode(MCRErrorCodeConstants.MCROBJECT_STILL_LINKED)
744 .withMessage("MCRObject " + id + " is still linked by other objects.")
745 .withDetail(activeLinks.toString())
746 .withCause(e)
747 .toException();
748 } catch (MCRAccessException e) {
749
750 throw MCRErrorResponse.fromStatus(Response.Status.FORBIDDEN.getStatusCode())
751 .withErrorCode(MCRErrorCodeConstants.MCROBJECT_NO_PERMISSION)
752 .withMessage("You may not delete MCRObject " + id + ".")
753 .withDetail(e.getMessage())
754 .withCause(e)
755 .toException();
756 }
757 return Response.noContent().build();
758 }
759
760 @PUT
761 @Path("/{" + PARAM_MCRID + "}/try")
762 @Operation(summary = "pre-flight target to test write operation on {" + PARAM_MCRID + "}",
763 tags = MCRRestUtils.TAG_MYCORE_OBJECT,
764 responses = {
765 @ApiResponse(responseCode = "202", description = "You have write permission"),
766 @ApiResponse(responseCode = "401",
767 description = "You do not have write permission and need to authenticate first"),
768 @ApiResponse(responseCode = "403", description = "You do not have write permission"),
769 })
770 public Response testUpdateObject(@PathParam(PARAM_MCRID) MCRObjectID id)
771 throws IOException {
772 return Response.status(Response.Status.ACCEPTED).build();
773 }
774
775 @DELETE
776 @Path("/{" + PARAM_MCRID + "}/try")
777 @Operation(summary = "pre-flight target to test delete operation on {" + PARAM_MCRID + "}",
778 tags = MCRRestUtils.TAG_MYCORE_OBJECT,
779 responses = {
780 @ApiResponse(responseCode = "202", description = "You have delete permission"),
781 @ApiResponse(responseCode = "401",
782 description = "You do not have delete permission and need to authenticate first"),
783 @ApiResponse(responseCode = "403", description = "You do not have delete permission"),
784 })
785 public Response testDeleteObject(@PathParam(PARAM_MCRID) MCRObjectID id)
786 throws IOException {
787 return Response.status(Response.Status.ACCEPTED).build();
788 }
789
790 @PUT
791 @Consumes(MediaType.TEXT_PLAIN)
792 @Produces({ MediaType.APPLICATION_JSON, MediaType.APPLICATION_XML })
793 @Path("/{" + PARAM_MCRID + "}/service/state")
794 @Operation(summary = "change state of object {" + PARAM_MCRID + "}",
795 tags = MCRRestUtils.TAG_MYCORE_OBJECT,
796 responses = {
797 @ApiResponse(responseCode = "204", description = "operation was successful"),
798 @ApiResponse(responseCode = "400", description = "Invalid state"),
799 @ApiResponse(responseCode = "404", description = "object is not found"),
800 })
801 @MCRRequireTransaction
802 @MCRApiDraft("MCRObjectState")
803 public Response setState(@PathParam(PARAM_MCRID) MCRObjectID id, String state) {
804
805 if (!MCRMetadataManager.exists(id)) {
806 throw MCRErrorResponse.fromStatus(Response.Status.NOT_FOUND.getStatusCode())
807 .withErrorCode(MCRErrorCodeConstants.MCROBJECT_NOT_FOUND)
808 .withMessage("MCRObject " + id + " not found")
809 .toException();
810 }
811 if (!state.isEmpty()) {
812 MCRCategoryID categState = new MCRCategoryID(
813 MCRConfiguration2.getString("MCR.Metadata.Service.State.Classification.ID").orElse("state"), state);
814 if (!MCRCategoryDAOFactory.getInstance().exist(categState)) {
815 throw MCRErrorResponse.fromStatus(Response.Status.BAD_REQUEST.getStatusCode())
816 .withErrorCode(MCRErrorCodeConstants.MCROBJECT_INVALID_STATE)
817 .withMessage("Category " + categState + " not found")
818 .toException();
819 }
820 }
821 final MCRObject mcrObject = MCRMetadataManager.retrieveMCRObject(id);
822 if (state.isEmpty()) {
823 mcrObject.getService().removeState();
824 } else {
825 mcrObject.getService().setState(state);
826 }
827 try {
828 MCRMetadataManager.update(mcrObject);
829 return Response.status(Response.Status.NO_CONTENT).build();
830 } catch (MCRAccessException e) {
831 throw MCRErrorResponse.fromStatus(Response.Status.FORBIDDEN.getStatusCode())
832 .withErrorCode(MCRErrorCodeConstants.MCROBJECT_NO_PERMISSION)
833 .withMessage("You may not modify or create metadata of MCRObject " + id + ".")
834 .withDetail(e.getMessage())
835 .withCause(e)
836 .toException();
837 }
838 }
839
840 @GET
841 @Produces({ MediaType.APPLICATION_JSON, MediaType.APPLICATION_XML })
842 @Path("/{" + PARAM_MCRID + "}/service/state")
843 @Operation(summary = "get state of object {" + PARAM_MCRID + "}",
844 tags = MCRRestUtils.TAG_MYCORE_OBJECT,
845 responses = {
846 @ApiResponse(responseCode = "307", description = "redirect to state category"),
847 @ApiResponse(responseCode = "204", description = "no state is set"),
848 @ApiResponse(responseCode = "404", description = "object is not found"),
849 })
850 @MCRApiDraft("MCRObjectState")
851 public Response getState(@PathParam(PARAM_MCRID) MCRObjectID id) {
852
853 if (!MCRMetadataManager.exists(id)) {
854 throw MCRErrorResponse.fromStatus(Response.Status.NOT_FOUND.getStatusCode())
855 .withErrorCode(MCRErrorCodeConstants.MCROBJECT_NOT_FOUND)
856 .withMessage("MCRObject " + id + " not found")
857 .toException();
858 }
859 final MCRCategoryID state = MCRMetadataManager.retrieveMCRObject(id).getService().getState();
860 if (state == null) {
861 return Response.noContent().build();
862 }
863 return Response.temporaryRedirect(
864 uriInfo.resolve(URI.create("classifications/" + state.getRootID() + "/" + state.getID())))
865 .build();
866 }
867
868 }