/******************************************************************************* * Copyright French Prime minister Office/SGMAP/DINSIC/Vitam Program (2015-2019) * * contact.vitam@culture.gouv.fr * * This software is a computer program whose purpose is to implement a digital archiving back-office system managing * high volumetry securely and efficiently. * * This software is governed by the CeCILL 2.1 license under French law and abiding by the rules of distribution of free * software. You can use, modify and/ or redistribute the software under the terms of the CeCILL 2.1 license as * circulated by CEA, CNRS and INRIA at the following URL "http://www.cecill.info". * * As a counterpart to the access to the source code and rights to copy, modify and redistribute granted by the license, * users are provided only with a limited warranty and the software's author, the holder of the economic rights, and the * successive licensors have only limited liability. * * In this respect, the user's attention is drawn to the risks associated with loading, using, modifying and/or * developing or reproducing the software by the user in light of its specific status of free software, that may mean * that it is complicated to manipulate, and that also therefore means that it is reserved for developers and * experienced professionals having in-depth computer knowledge. Users are therefore encouraged to load and test the * software's suitability as regards their requirements in conditions enabling the security of their systems and/or data * to be ensured and, more generally, to use and operate it in the same conditions as regards security. * * The fact that you are presently reading this means that you have had knowledge of the CeCILL 2.1 license and that you * accept its terms. *******************************************************************************/ package fr.gouv.vitam.storage.offers.common.rest; import java.io.IOException; import java.io.InputStream; import java.util.List; import javax.ws.rs.Consumes; import javax.ws.rs.DELETE; import javax.ws.rs.GET; import javax.ws.rs.HEAD; import javax.ws.rs.HeaderParam; import javax.ws.rs.POST; import javax.ws.rs.PUT; import javax.ws.rs.Path; import javax.ws.rs.PathParam; import javax.ws.rs.Produces; import javax.ws.rs.container.AsyncResponse; import javax.ws.rs.container.Suspended; import javax.ws.rs.core.Context; import javax.ws.rs.core.HttpHeaders; import javax.ws.rs.core.MediaType; import javax.ws.rs.core.Response; import javax.ws.rs.core.Response.Status; import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.node.ObjectNode; import com.google.common.base.Strings; import fr.gouv.vitam.common.CommonMediaType; import fr.gouv.vitam.common.GlobalDataRest; import fr.gouv.vitam.common.client.VitamRequestIterator; import fr.gouv.vitam.common.digest.DigestType; import fr.gouv.vitam.common.error.VitamCode; import fr.gouv.vitam.common.error.VitamCodeHelper; import fr.gouv.vitam.common.error.VitamError; import fr.gouv.vitam.common.guid.GUIDFactory; import fr.gouv.vitam.common.logging.VitamLogger; import fr.gouv.vitam.common.logging.VitamLoggerFactory; import fr.gouv.vitam.common.model.RequestResponseOK; import fr.gouv.vitam.common.server.application.AsyncInputStreamHelper; import fr.gouv.vitam.common.server.application.resources.ApplicationStatusResource; import fr.gouv.vitam.common.storage.constants.ErrorMessage; import fr.gouv.vitam.common.stream.SizedInputStream; import fr.gouv.vitam.common.stream.StreamUtils; import fr.gouv.vitam.common.thread.VitamThreadPoolExecutor; import fr.gouv.vitam.common.thread.VitamThreadUtils; import fr.gouv.vitam.storage.driver.model.StorageMetadatasResult; import fr.gouv.vitam.storage.engine.common.StorageConstants; import fr.gouv.vitam.storage.engine.common.model.DataCategory; import fr.gouv.vitam.storage.engine.common.model.ObjectInit; import fr.gouv.vitam.storage.engine.common.model.response.RequestResponseError; import fr.gouv.vitam.storage.offers.common.core.DefaultOfferServiceImpl; import fr.gouv.vitam.workspace.api.exception.ContentAddressableStorageAlreadyExistException; import fr.gouv.vitam.workspace.api.exception.ContentAddressableStorageException; import fr.gouv.vitam.workspace.api.exception.ContentAddressableStorageNotFoundException; import fr.gouv.vitam.workspace.api.exception.ContentAddressableStorageServerException; /** * Default offer REST Resource */ @Path("/offer/v1") @javax.ws.rs.ApplicationPath("webresources") public class DefaultOfferResource extends ApplicationStatusResource { private static final String MISSING_THE_TENANT_ID_X_TENANT_ID = "Missing the tenant ID (X-Tenant-Id) or wrong object Type"; private static final VitamLogger LOGGER = VitamLoggerFactory.getInstance(DefaultOfferResource.class); private static final String DEFAULT_OFFER_MODULE = "DEFAULT_OFFER"; private static final String CODE_VITAM = "code_vitam"; private static final String MISSING_X_DIGEST_ALGORITHM = "Missing the digest type (X-digest-algorithm)"; private static final String MISSING_X_DIGEST = "Missing the type (X-digest)"; /** * Constructor */ public DefaultOfferResource() { LOGGER.debug("DefaultOfferResource initialized"); } /** * Get the information on the offer objects collection (free and used * capacity, etc) * * @param xTenantId * @param type * The container type * @return information on the offer objects collection * */ // TODO P1 : review java method name // FIXME P1 il manque le /container/id/ @HEAD @Path("/objects/{type}") @Produces(MediaType.APPLICATION_JSON) public Response getCapacity(@HeaderParam(GlobalDataRest.X_TENANT_ID) String xTenantId, @PathParam("type") DataCategory type) { if (Strings.isNullOrEmpty(xTenantId)) { LOGGER.error(MISSING_THE_TENANT_ID_X_TENANT_ID); return Response.status(Response.Status.BAD_REQUEST).build(); } final String containerName = buildContainerName(type, xTenantId); try { ObjectNode result = (ObjectNode) DefaultOfferServiceImpl.getInstance().getCapacity(containerName); Response.ResponseBuilder response = Response.status(Status.OK); response.header("X-Usable-Space", result.get("usableSpace")); response.header("X-Used-Space", result.get("usedSpace")); response.header(GlobalDataRest.X_TENANT_ID, xTenantId); return response.build(); } catch (final ContentAddressableStorageNotFoundException exc) { LOGGER.error(exc); return Response.status(Response.Status.SERVICE_UNAVAILABLE).build(); } catch (final ContentAddressableStorageServerException exc) { LOGGER.error(exc); return Response.status(Status.INTERNAL_SERVER_ERROR).build(); } } /** * Get container object list * * @param xcursor * if true means new query, if false means end of query from * client side * @param xcursorId * if present, means continue on cursor * @param xTenantId * the tenant id * @param type * object type * @return an iterator with each object metadata (actually only the id) */ @GET @Path("/objects/{type}") @Consumes(MediaType.APPLICATION_JSON) @Produces(MediaType.APPLICATION_JSON) public Response getContainerList(@HeaderParam(GlobalDataRest.X_CURSOR) boolean xcursor, @HeaderParam(GlobalDataRest.X_CURSOR_ID) String xcursorId, @HeaderParam(GlobalDataRest.X_TENANT_ID) String xTenantId, @PathParam("type") DataCategory type) { if (Strings.isNullOrEmpty(xTenantId)) { LOGGER.error(MISSING_THE_TENANT_ID_X_TENANT_ID); final Response.ResponseBuilder builder = Response.status(Status.BAD_REQUEST); return VitamRequestIterator.setHeaders(builder, xcursor, null).build(); } Status status; String cursorId = xcursorId; if (VitamRequestIterator.isEndOfCursor(xcursor, xcursorId)) { DefaultOfferServiceImpl.getInstance().finalizeCursor(buildContainerName(type, xTenantId), xcursorId); final Response.ResponseBuilder builder = Response.status(Status.NO_CONTENT); return VitamRequestIterator.setHeaders(builder, xcursor, null).build(); } if (VitamRequestIterator.isNewCursor(xcursor, xcursorId)) { try { cursorId = DefaultOfferServiceImpl.getInstance().createCursor(buildContainerName(type, xTenantId)); } catch (ContentAddressableStorageNotFoundException | ContentAddressableStorageServerException exc) { LOGGER.error(exc); status = Status.INTERNAL_SERVER_ERROR; final Response.ResponseBuilder builder = Response.status(status) .entity(new VitamError(status.name()).setHttpCode(status.getStatusCode()).setContext("default-offer") .setState("code_vitam").setMessage(status.getReasonPhrase()).setDescription(exc.getMessage())); return VitamRequestIterator.setHeaders(builder, xcursor, null).build(); } } final RequestResponseOK<JsonNode> responseOK = new RequestResponseOK<JsonNode>(); if (DefaultOfferServiceImpl.getInstance().hasNext(buildContainerName(type, xTenantId), cursorId)) { try { List<JsonNode> list = DefaultOfferServiceImpl.getInstance().next(buildContainerName(type, xTenantId), cursorId); responseOK.addAllResults(list); final Response.ResponseBuilder builder = Response .status(DefaultOfferServiceImpl.getInstance().hasNext(buildContainerName(type, xTenantId), cursorId) ? Status.PARTIAL_CONTENT : Status.OK) .entity(responseOK.setHits(list.size(), 0, list.size())); return VitamRequestIterator.setHeaders(builder, xcursor, cursorId).build(); } catch (ContentAddressableStorageNotFoundException exc) { LOGGER.error(exc); status = Status.INTERNAL_SERVER_ERROR; final Response.ResponseBuilder builder = Response.status(status) .entity(new VitamError(status.name()).setHttpCode(status.getStatusCode()).setContext(DEFAULT_OFFER_MODULE) .setState(CODE_VITAM).setMessage(status.getReasonPhrase()).setDescription(exc.getMessage())); return VitamRequestIterator.setHeaders(builder, xcursor, null).build(); } } else { DefaultOfferServiceImpl.getInstance().finalizeCursor(buildContainerName(type, xTenantId), xcursorId); final Response.ResponseBuilder builder = Response.status(Status.NO_CONTENT); return VitamRequestIterator.setHeaders(builder, xcursor, null).build(); } } /** * Count the number of objects on the offer objects defined container * (exlude directories) * * @param xTenantId * @param type * @return number of binary objects in the container * */ @GET // FIXME Later we should count in a standard get request (no /count in path) // with a DSL that specify a count // operation (aggregate) @Path("/objects/{type}/count") @Produces(MediaType.APPLICATION_JSON) public Response countObjects(@HeaderParam(GlobalDataRest.X_TENANT_ID) String xTenantId, @PathParam("type") DataCategory type) { if (Strings.isNullOrEmpty(xTenantId)) { LOGGER.error(MISSING_THE_TENANT_ID_X_TENANT_ID); return Response.status(Response.Status.BAD_REQUEST).build(); } final String containerName = buildContainerName(type, xTenantId); try { final JsonNode result = DefaultOfferServiceImpl.getInstance().countObjects(containerName); return Response.status(Response.Status.OK).entity(result).build(); } catch (final ContentAddressableStorageNotFoundException exc) { LOGGER.error(exc); return Response.status(Response.Status.NOT_FOUND).build(); } catch (final ContentAddressableStorageServerException exc) { LOGGER.error(exc); return Response.status(Status.INTERNAL_SERVER_ERROR).build(); } } /** * Get the object data or digest from its id. * <p> * HEADER X-Tenant-Id (mandatory) : tenant's identifier HEADER "X-type" * (optional) : data (dfault) or digest * </p> * * @param type * Object type * @param objectId * object id :.+ in order to get all path if some '/' are * provided * @param headers * http header * @param asyncResponse * async response * @throws IOException * when there is an error of get object */ @GET @Path("/objects/{type}/{id_object:.+}") @Consumes(MediaType.APPLICATION_JSON) @Produces({ MediaType.APPLICATION_OCTET_STREAM, CommonMediaType.ZIP }) public void getObject(@PathParam("type") DataCategory type, @PathParam("id_object") String objectId, @Context HttpHeaders headers, @Suspended final AsyncResponse asyncResponse) throws IOException { VitamThreadPoolExecutor.getDefaultExecutor().execute(new Runnable() { @Override public void run() { getObjectAsync(type, objectId, headers, asyncResponse); } }); } /** * Initialise a new object. * <p> * HEADER X-Command (mandatory) : INIT <br> * HEADER X-Tenant-Id (mandatory) : tenant's identifier * </p> * * @param type * New object's type * @param objectGUID * the GUID Of the object * @param headers * http header * @param objectInit * data for object creation * @return structured response with the object id */ // TODO - us#1982 - to be changed with this story - tenantId to stay in the // header but path (type unit or object) in // the uri @POST @Path("/objects/{type}/{guid:.+}") @Consumes(MediaType.APPLICATION_JSON) @Produces(MediaType.APPLICATION_JSON) public Response postObject(@PathParam("guid") String objectGUID, @PathParam("type") DataCategory type, @Context HttpHeaders headers, ObjectInit objectInit) { final String xTenantId = headers.getHeaderString(GlobalDataRest.X_TENANT_ID); if (objectInit == null) { LOGGER.error(MISSING_THE_TENANT_ID_X_TENANT_ID); return Response.status(Response.Status.BAD_REQUEST).build(); } if (Strings.isNullOrEmpty(xTenantId)) { LOGGER.error(MISSING_THE_TENANT_ID_X_TENANT_ID); return Response.status(Response.Status.BAD_REQUEST).build(); } final String containerName = buildContainerName(type, xTenantId); if (!objectInit.getType().equals(type)) { LOGGER.error(MISSING_THE_TENANT_ID_X_TENANT_ID); return Response.status(Response.Status.BAD_REQUEST).build(); } final String xCommandHeader = headers.getHeaderString(GlobalDataRest.X_COMMAND); if (xCommandHeader == null || !xCommandHeader.equals(StorageConstants.COMMAND_INIT)) { LOGGER.error("Missing the INIT required command (X-Command header)"); return Response.status(Response.Status.BAD_REQUEST).build(); } try { final ObjectInit objectInitFilled = DefaultOfferServiceImpl.getInstance().initCreateObject(containerName, objectInit, objectGUID); LOGGER.info("ContainerName: " + containerName + " ObjectGUID " + objectGUID); return Response.status(Response.Status.CREATED).entity(objectInitFilled).build(); } catch (final ContentAddressableStorageAlreadyExistException e) { LOGGER.error("ContainerName: " + containerName + " ObjectGUID " + objectGUID, e); return Response.status(Response.Status.CONFLICT).build(); } catch (final ContentAddressableStorageException exc) { LOGGER.error(exc); return Response.status(Response.Status.INTERNAL_SERVER_ERROR).build(); } } /** * Write a new chunk in an object or end its creation.<br> * Replaces units and objectGroups object type if exist * <p> * HEADER X-Command (mandatory) : WRITE/END HEADER X-Tenant-Id (mandatory) : * tenant's identifier * </p> * * @param type * Object type to update * @param objectId * object id * @param headers * http header * @param input * object data * @return structured response with the object id (and new digest ?) */ // TODO - us#1982 - to be changed with this story - tenantId to stay in the // header but path (type unit or object) in // the uri @PUT @Path("/objects/{type}/{id:.+}") @Consumes(MediaType.APPLICATION_OCTET_STREAM) @Produces(MediaType.APPLICATION_JSON) public Response putObject(@PathParam("type") DataCategory type, @PathParam("id") String objectId, @Context HttpHeaders headers, InputStream input) { try { final String xTenantId = headers.getHeaderString(GlobalDataRest.X_TENANT_ID); if (Strings.isNullOrEmpty(xTenantId)) { LOGGER.error(MISSING_THE_TENANT_ID_X_TENANT_ID); return Response.status(Response.Status.BAD_REQUEST).build(); } final String containerName = buildContainerName(type, xTenantId); final String xCommandHeader = headers.getHeaderString(GlobalDataRest.X_COMMAND); if (xCommandHeader == null || !xCommandHeader.equals(StorageConstants.COMMAND_WRITE) && !xCommandHeader.equals(StorageConstants.COMMAND_END)) { LOGGER.error("Missing the WRITE or END required command (X-Command header), {} found", xCommandHeader); return Response.status(Response.Status.BAD_REQUEST).build(); } try { final SizedInputStream sis = new SizedInputStream(input); final String digest = DefaultOfferServiceImpl.getInstance().createObject(containerName, objectId, sis, xCommandHeader.equals(StorageConstants.COMMAND_END)); return Response.status(Response.Status.CREATED) .entity("{\"digest\":\"" + digest + "\",\"size\":\"" + sis.getSize() + "\"}").build(); } catch (IOException | ContentAddressableStorageException exc) { LOGGER.error("Cannot create object", exc); return Response.status(Response.Status.INTERNAL_SERVER_ERROR).build(); } } catch (Exception e) { LOGGER.error(e); return Response.status(Response.Status.INTERNAL_SERVER_ERROR).build(); } finally { StreamUtils.closeSilently(input); } } /** * Delete an Object * * @param xTenantId * the tenantId * @param xDigest * the digest of the object to delete * @param xDigestAlgorithm * the digest algorithm * @param type * Object type to delete * @param idObject * the id of the object to be tested * @return the response with a specific HTTP status */ @DELETE @Path("/objects/{type}/{id:.+}") @Produces(MediaType.APPLICATION_JSON) public Response deleteObject(@HeaderParam(GlobalDataRest.X_TENANT_ID) String xTenantId, @HeaderParam(GlobalDataRest.X_DIGEST) String xDigest, @HeaderParam(GlobalDataRest.X_DIGEST_ALGORITHM) String xDigestAlgorithm, @PathParam("type") DataCategory type, @PathParam("id") String idObject) { if (Strings.isNullOrEmpty(xTenantId)) { LOGGER.error(MISSING_THE_TENANT_ID_X_TENANT_ID); return Response.status(Response.Status.BAD_REQUEST).build(); } if (Strings.isNullOrEmpty(xDigestAlgorithm)) { LOGGER.error(MISSING_X_DIGEST_ALGORITHM); return Response.status(Response.Status.BAD_REQUEST).build(); } if (Strings.isNullOrEmpty(xDigest)) { LOGGER.error(MISSING_X_DIGEST); return Response.status(Response.Status.BAD_REQUEST).build(); } try { VitamThreadUtils.getVitamSession().setRequestId(GUIDFactory.newRequestIdGUID(Integer.parseInt(xTenantId))); final String containerName = buildContainerName(type, xTenantId); DefaultOfferServiceImpl.getInstance().deleteObject(containerName, idObject, xDigest, DigestType.fromValue(xDigestAlgorithm)); return Response.status(Response.Status.OK) .entity("{\"id\":\"" + idObject + "\",\"status\":\"" + Response.Status.OK.toString() + "\"}").build(); } catch (ContentAddressableStorageNotFoundException e) { LOGGER.error(e); return Response.status(Response.Status.NOT_FOUND).build(); } catch (ContentAddressableStorageException e) { LOGGER.error(e); return Response.status(Status.INTERNAL_SERVER_ERROR).build(); } } /** * Test the existence of an object * * HEADER X-Tenant-Id (mandatory) : tenant's identifier * * @param type * Object type to test * @param idObject * the id of the object to be tested * @param xTenantId * the id of the tenant * @param xDigest * the digest * @param xDigestAlgorithm * the digest algorithm * @return the response with a specific HTTP status. If none of DIGEST or * DIGEST_ALGORITHM headers is given, an existence test is done and * can return 204/404 as response. If only DIGEST or only * DIGEST_ALGORITHM header is given, a not implemented exception is * thrown. Later, this should respond with 200/409. If both DIGEST * and DIGEST_ALGORITHM header are given, a full digest check is * done and can return 200/409 as response */ @HEAD @Path("/objects/{type}/{id:.+}") public Response headObject(@PathParam("type") DataCategory type, @PathParam("id") String idObject, @HeaderParam(GlobalDataRest.X_TENANT_ID) String xTenantId, @HeaderParam(GlobalDataRest.X_DIGEST) String xDigest, @HeaderParam(GlobalDataRest.X_DIGEST_ALGORITHM) String xDigestAlgorithm) { if (Strings.isNullOrEmpty(xTenantId)) { LOGGER.error(MISSING_THE_TENANT_ID_X_TENANT_ID); return Response.status(Response.Status.BAD_REQUEST).build(); } String containerName = buildContainerName(type, xTenantId); Boolean checkDigest = !Strings.isNullOrEmpty(xDigest); Boolean checkAlgo = !Strings.isNullOrEmpty(xDigestAlgorithm); try { boolean objectIsOK; if (checkDigest && !checkAlgo) { objectIsOK = DefaultOfferServiceImpl.getInstance().checkDigest(containerName, idObject, xDigest); } else if (checkAlgo && !checkDigest) { objectIsOK = DefaultOfferServiceImpl.getInstance().checkDigestAlgorithm(containerName, idObject, DigestType.fromValue(xDigestAlgorithm)); } else if (checkAlgo && checkDigest) { objectIsOK = DefaultOfferServiceImpl.getInstance().checkObject(containerName, idObject, xDigest, DigestType.fromValue(xDigestAlgorithm)); } else { if (DefaultOfferServiceImpl.getInstance().isObjectExist(containerName, idObject)) { return Response.status(Response.Status.NO_CONTENT).build(); } else { return Response.status(Response.Status.NOT_FOUND).build(); } } if (objectIsOK) { return Response.status(Status.OK).build(); } else { return Response.status(Status.CONFLICT).build(); } } catch (final ContentAddressableStorageException e) { LOGGER.error(ErrorMessage.INTERNAL_SERVER_ERROR.getMessage(), e); return Response.status(Status.INTERNAL_SERVER_ERROR).build(); } } @GET @Path("/objects/{type}/{id:.+}/metadatas") @Produces(MediaType.APPLICATION_JSON) public Response getObjectMetadata(@PathParam("type") DataCategory type, @PathParam("id") String idObject, @HeaderParam(GlobalDataRest.X_TENANT_ID) String xTenantId) { if (Strings.isNullOrEmpty(xTenantId)) { LOGGER.error(MISSING_THE_TENANT_ID_X_TENANT_ID); return Response.status(Response.Status.BAD_REQUEST).build(); } final String containerName = buildContainerName(type, xTenantId); try { StorageMetadatasResult result = DefaultOfferServiceImpl.getInstance().getMetadatas(containerName, idObject); return Response.status(Response.Status.OK).entity(result).build(); } catch (ContentAddressableStorageNotFoundException e) { LOGGER.error(e); return Response.status(Response.Status.NOT_FOUND).build(); } catch (ContentAddressableStorageException | IOException e) { LOGGER.error(e); return Response.status(Status.INTERNAL_SERVER_ERROR).build(); } } private void getObjectAsync(DataCategory type, String objectId, HttpHeaders headers, AsyncResponse asyncResponse) { try { final String xTenantId = headers.getHeaderString(GlobalDataRest.X_TENANT_ID); if (Strings.isNullOrEmpty(xTenantId)) { LOGGER.error(MISSING_THE_TENANT_ID_X_TENANT_ID); AsyncInputStreamHelper.asyncResponseResume(asyncResponse, Response.status(Status.PRECONDITION_FAILED).build()); return; } final String containerName = buildContainerName(type, xTenantId); DefaultOfferServiceImpl.getInstance().getObject(containerName, objectId, asyncResponse); } catch (final ContentAddressableStorageNotFoundException e) { LOGGER.error(e); buildErrorResponseAsync(VitamCode.STORAGE_NOT_FOUND, asyncResponse); } catch (final ContentAddressableStorageException e) { LOGGER.error(e); buildErrorResponseAsync(VitamCode.STORAGE_TECHNICAL_INTERNAL_ERROR, asyncResponse); } } /** * Add error response in async response using with vitamCode * * @param vitamCode * vitam error Code * @param asyncResponse * asynchronous response */ private void buildErrorResponseAsync(VitamCode vitamCode, AsyncResponse asyncResponse) { AsyncInputStreamHelper.asyncResponseResume(asyncResponse, Response.status(vitamCode.getStatus()).entity(new RequestResponseError().setError( new VitamError(VitamCodeHelper.getCode(vitamCode)) .setContext(vitamCode.getService().getName()) .setState(vitamCode.getDomain().getName()) .setMessage(vitamCode.getMessage()) .setDescription(vitamCode.getMessage())) .toString()).build()); } private VitamError getErrorEntity(Status status) { return new VitamError(status.name()).setHttpCode(status.getStatusCode()).setContext(DEFAULT_OFFER_MODULE) .setState(CODE_VITAM).setMessage(status.getReasonPhrase()).setDescription(status.getReasonPhrase()); } private String buildContainerName(DataCategory type, String tenantId) { if (type == null || Strings.isNullOrEmpty(type.getFolder()) || Strings.isNullOrEmpty(tenantId)) { return null; } return tenantId + "_" + type.getFolder(); } }