ContextController.java
/*
* Copyright (c) 2007-2026 MetaSolutions AB
*
* 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 org.entrystore.rest.springboot.controller;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.media.Content;
import io.swagger.v3.oas.annotations.media.Schema;
import io.swagger.v3.oas.annotations.media.SchemaProperty;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.entrystore.Context;
import org.entrystore.Entry;
import org.entrystore.EntryType;
import org.entrystore.GraphType;
import org.entrystore.rest.springboot.model.api.CreateEntryRequestBody;
import org.entrystore.rest.springboot.model.api.CreateEntryResponse;
import org.entrystore.rest.springboot.model.exception.BadRequestException;
import org.entrystore.rest.springboot.service.ContextService;
import org.entrystore.rest.springboot.service.EntryService;
import org.entrystore.rest.springboot.util.HttpUtil;
import org.entrystore.rest.springboot.util.MultipartUtil;
import org.springframework.core.io.InputStreamResource;
import org.springframework.http.HttpHeaders;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.multipart.MultipartFile;
import org.springframework.web.multipart.MultipartRequest;
import java.io.ByteArrayInputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
import java.net.URI;
import java.util.List;
@Slf4j
@RestController
@RequiredArgsConstructor
public class ContextController {
private final ContextService contextService;
private final EntryService entryService;
@Operation(summary = "Returns an array of IDs of a context's entries")
@GetMapping(path = "/{context-id:(?!favicon\\.ico$).+}", produces = {MediaType.APPLICATION_JSON_VALUE, MediaType.APPLICATION_XML_VALUE})
public List<String> getContextEntries(
@PathVariable("context-id") String contextId,
@RequestParam(required = false, name = "entryname") String entryName,
@RequestParam(required = false, name = "deleted") String deletedEntries
) {
return contextService.getContextEntries(contextId, deletedEntries != null, entryName);
}
@PreAuthorize("hasAnyRole('USER','ADMIN')")
@Operation(summary = "Creates a new entry inside the given context")
@PostMapping(path = "/{context-id:(?!favicon\\.ico$).+}", produces = {MediaType.APPLICATION_JSON_VALUE, MediaType.APPLICATION_XML_VALUE})
public ResponseEntity<CreateEntryResponse> createEntry(
@PathVariable("context-id") String contextId,
@RequestParam(required = false, name = "id") String entryId,
@RequestParam(required = false, name = "entrytype") EntryType entryType,
@RequestParam(required = false, name = "graphtype", defaultValue = "none") GraphType graphType,
@RequestParam(required = false, name = "resource") URI resourceUri,
@RequestParam(required = false, name = "list") URI listUri,
@RequestParam(required = false) URI groupURI,
@RequestParam(required = false, name = "cached-external-metadata") URI cachedExternalMetadataUri,
@RequestParam(required = false, name = "informationresource") String informationResource,
@RequestParam(required = false, name = "template") URI templateUri,
@RequestBody(required = false) CreateEntryRequestBody body) {
if (isGraphTypeForbidden(graphType)) {
throw new BadRequestException("Pipeline results may only be created by Pipelines");
}
Context context = contextService.getContextOrThrow(contextId);
Entry entry = entryService.createEntry(context, entryId, entryType, graphType, resourceUri, listUri,
groupURI, cachedExternalMetadataUri, informationResource, templateUri, body);
CreateEntryResponse responseBody = new CreateEntryResponse(entry.getId());
ResponseEntity.BodyBuilder bodyBuilder = ResponseEntity.created(entry.getEntryURI());
HttpUtil.updateResponseWithModificationDateAndETag(bodyBuilder, entry.getModifiedDate());
return bodyBuilder.body(responseBody);
}
@PreAuthorize("hasRole('ADMIN')")
@Operation(summary = "Exports a context")
@GetMapping(
path = "/{context-id}/export"
// we should add the 'produces' MediaType to the endpoint to clearly inform users what type of data is returned
// however, then the behavior differs from Restlet, maybe tweaking Spring-boot content-negotiation strategy will help
//produces = "application/zip"
)
public ResponseEntity<InputStreamResource> createContextExport(
@PathVariable("context-id") String contextId,
@RequestParam(required = false) String metadataOnly,
@RequestParam(required = false) MediaType rdfFormat
) throws FileNotFoundException {
Context context = contextService.getContextOrThrow(contextId);
File zipFile = contextService.exportContextToAZipFile(context, metadataOnly != null,
rdfFormat != null ? rdfFormat.toString() : null);
InputStreamResource fileStream = new InputStreamResource(new FileInputStream(zipFile));
return ResponseEntity.ok()
.header(HttpHeaders.CONTENT_DISPOSITION, "attachment; filename=\"context_" + contextId + "_export.zip\"")
.contentType(MediaType.valueOf("application/zip"))
.contentLength(zipFile.length())
.body(fileStream);
}
@Operation(summary = "Import of a single context")
@PostMapping(
path = "/{context-id}/import",
produces = MediaType.TEXT_HTML_VALUE)
public String importContextFromZipFile(
@PathVariable("context-id") String contextId,
@RequestBody byte[] zipFile) {
Context context = contextService.getContextOrThrow(contextId);
contextService.importContextDataFromFile(context, new ByteArrayInputStream(zipFile));
return "<textarea></textarea>";
}
@Operation(
summary = "Import of a single context",
description = "The ZIP file should be sent as a file part of a multipart/form-data request. It may carry any " +
"part name: a part named \"file\" is used when it carries a filename, otherwise the first part " +
"carrying a filename, otherwise the first part carrying content.")
@io.swagger.v3.oas.annotations.parameters.RequestBody(
content = @Content(
mediaType = MediaType.MULTIPART_FORM_DATA_VALUE,
schema = @Schema(type = "object"),
schemaProperties = @SchemaProperty(
name = "file",
schema = @Schema(type = "string", format = "binary"))))
@AcceptsMultipart
@PostMapping(
path = "/{context-id}/import",
consumes = MediaType.MULTIPART_FORM_DATA_VALUE,
produces = MediaType.TEXT_HTML_VALUE)
public String importContextFromMultipartForm(
@PathVariable("context-id") String contextId,
@Parameter(hidden = true) MultipartRequest request) throws IOException {
MultipartFile file = MultipartUtil.firstFilePart(request)
.orElseThrow(() -> new BadRequestException("Multipart request contains no file part"));
Context context = contextService.getContextOrThrow(contextId);
try (InputStream inputStream = file.getInputStream()) {
contextService.importContextDataFromFile(context, inputStream);
}
return "<textarea></textarea>";
}
/**
* Returns false if the Graph Type provided in the parameters
* cannot be used for manually created entries
*
* @return True if Graph Type is forbidden/blacklisted.
*/
private boolean isGraphTypeForbidden(GraphType graphType) {
// Pipeline results may only be created by Pipelines
return GraphType.PipelineResult == graphType;
}
}