/******************************************************************************* * 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.engine.server.distribution.impl; import java.io.InputStream; import java.security.DigestInputStream; import java.time.LocalDateTime; import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Map.Entry; import java.util.Properties; import java.util.TreeMap; import java.util.concurrent.ExecutionException; import java.util.concurrent.ExecutorService; import java.util.concurrent.Future; import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeoutException; import javax.ws.rs.container.AsyncResponse; import javax.ws.rs.core.MediaType; import javax.ws.rs.core.Response; import javax.ws.rs.core.Response.ResponseBuilder; import javax.ws.rs.core.Response.Status; import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.node.ArrayNode; import com.fasterxml.jackson.databind.node.ObjectNode; import fr.gouv.vitam.common.LocalDateUtil; import fr.gouv.vitam.common.ParametersChecker; import fr.gouv.vitam.common.VitamConfiguration; import fr.gouv.vitam.common.digest.Digest; 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.guid.GUID; import fr.gouv.vitam.common.json.JsonHandler; import fr.gouv.vitam.common.logging.VitamLogger; import fr.gouv.vitam.common.logging.VitamLoggerFactory; import fr.gouv.vitam.common.parameter.ParameterHelper; import fr.gouv.vitam.common.server.application.AsyncInputStreamHelper; import fr.gouv.vitam.common.server.application.VitamHttpHeader; import fr.gouv.vitam.common.stream.MultipleInputStreamHandler; import fr.gouv.vitam.common.thread.VitamThreadPoolExecutor; import fr.gouv.vitam.storage.driver.Connection; import fr.gouv.vitam.storage.driver.Driver; import fr.gouv.vitam.storage.driver.exception.StorageDriverException; import fr.gouv.vitam.storage.driver.exception.StorageObjectAlreadyExistsException; import fr.gouv.vitam.storage.driver.model.StorageGetResult; import fr.gouv.vitam.storage.driver.model.StorageListRequest; import fr.gouv.vitam.storage.driver.model.StorageObjectRequest; import fr.gouv.vitam.storage.driver.model.StoragePutRequest; import fr.gouv.vitam.storage.driver.model.StoragePutResult; import fr.gouv.vitam.storage.driver.model.StorageRemoveRequest; import fr.gouv.vitam.storage.driver.model.StorageRemoveResult; import fr.gouv.vitam.storage.engine.common.exception.StorageDriverNotFoundException; import fr.gouv.vitam.storage.engine.common.exception.StorageException; import fr.gouv.vitam.storage.engine.common.exception.StorageNotFoundException; import fr.gouv.vitam.storage.engine.common.exception.StorageTechnicalException; import fr.gouv.vitam.storage.engine.common.model.DataCategory; import fr.gouv.vitam.storage.engine.common.model.request.ObjectDescription; import fr.gouv.vitam.storage.engine.common.model.response.StoredInfoResult; import fr.gouv.vitam.storage.engine.common.referential.StorageOfferProvider; import fr.gouv.vitam.storage.engine.common.referential.StorageOfferProviderFactory; import fr.gouv.vitam.storage.engine.common.referential.StorageStrategyProvider; import fr.gouv.vitam.storage.engine.common.referential.StorageStrategyProviderFactory; import fr.gouv.vitam.storage.engine.common.referential.model.HotStrategy; import fr.gouv.vitam.storage.engine.common.referential.model.OfferReference; import fr.gouv.vitam.storage.engine.common.referential.model.StorageOffer; import fr.gouv.vitam.storage.engine.common.referential.model.StorageStrategy; import fr.gouv.vitam.storage.engine.server.distribution.StorageDistribution; import fr.gouv.vitam.storage.engine.server.logbook.StorageLogbook; import fr.gouv.vitam.storage.engine.server.logbook.StorageLogbookFactory; import fr.gouv.vitam.storage.engine.server.logbook.parameters.StorageLogbookOutcome; import fr.gouv.vitam.storage.engine.server.logbook.parameters.StorageLogbookParameterName; import fr.gouv.vitam.storage.engine.server.logbook.parameters.StorageLogbookParameters; import fr.gouv.vitam.storage.engine.server.rest.StorageConfiguration; import fr.gouv.vitam.storage.engine.server.spi.DriverManager; import fr.gouv.vitam.workspace.api.exception.ContentAddressableStorageNotFoundException; import fr.gouv.vitam.workspace.api.exception.ContentAddressableStorageServerException; import fr.gouv.vitam.workspace.client.WorkspaceClient; import fr.gouv.vitam.workspace.client.WorkspaceClientFactory; /** * StorageDistribution service Implementation process continue if needed) */ // TODO P1: see what to do with RuntimeException (catch it and log it to let the public class StorageDistributionImpl implements StorageDistribution { private static final String DEFAULT_SIZE_WHEN_UNKNOWN = "1000000"; private static final int DEFAULT_MINIMUM_TIMEOUT = 10000; private static final String STRATEGY_ID_IS_MANDATORY = "Strategy id is mandatory"; public static final String CATEGORY_IS_MANDATORY = "Category (object type) is mandatory"; private static final VitamLogger LOGGER = VitamLoggerFactory.getInstance(StorageDistributionImpl.class); private static final StorageStrategyProvider STRATEGY_PROVIDER = StorageStrategyProviderFactory.getDefaultProvider(); private static final StorageOfferProvider OFFER_PROVIDER = StorageOfferProviderFactory.getDefaultProvider(); private static final String NOT_IMPLEMENTED_MSG = "Not yet implemented"; private static final int NB_RETRY = 3; private static final String SIZE_KEY = "size"; private static final String STREAM_KEY = "stream"; /** * Global pool thread */ static final ExecutorService executor = new VitamThreadPoolExecutor(); /** * Used to wait for all task submission (executorService) */ private static final long threadSleep = 10; private final String urlWorkspace; private final Integer millisecondsPerKB; // TODO P2 see API // TODO P2 : later, the digest type may be retrieve via REST parameters. Fot // the moment (as of US 72 dev) there is // no // specification about that private final DigestType digestType; // FOR JUNIT TEST ONLY (TODO P1: review WorkspaceClientFactory to offer a // mocked WorkspaceClient) private final WorkspaceClient mockedWorkspaceClient; /** * Constructs the service with a given configuration * * @param configuration the configuration of the storage */ public StorageDistributionImpl(StorageConfiguration configuration) { ParametersChecker.checkParameter("Storage service configuration is mandatory", configuration); urlWorkspace = configuration.getUrlWorkspace(); WorkspaceClientFactory.changeMode(urlWorkspace); millisecondsPerKB = configuration.getTimeoutMsPerKB(); mockedWorkspaceClient = null; // TODO P2 : a real design discussion is needed : should we force it ? // Should we negociate it with the offer ? // TODO P2 Might be negotiated but limited to available digestType from // Vitam (MD5, SHA-1, SHA-256, SHA-512, // ...) // Just to note, I prefer SHA-512 (more CPU but more accurate and // already the default for Vitam, notably to // allow check of duplicated files) digestType = VitamConfiguration.getDefaultDigestType(); } /** * For JUnit ONLY * * @param wkClient a custom instance of workspace client * @param digestType a custom digest */ StorageDistributionImpl(WorkspaceClient wkClient, DigestType digestType) { urlWorkspace = null; millisecondsPerKB = 100; mockedWorkspaceClient = wkClient; this.digestType = digestType; } // TODO P1 : review design : for the moment we handle // createObjectDescription AND jsonData in the same params but // they should not be both resent at the same time. Maybe encapsulate or // create 2 methods // TODO P1 : refactor me ! // FIXME SHOULD not be synchronized but instability needs it @Override public StoredInfoResult storeData(String strategyId, String objectId, ObjectDescription createObjectDescription, DataCategory category, String requester) throws StorageException, StorageObjectAlreadyExistsException { // Check input params Integer tenantId = ParameterHelper.getTenantParameter(); checkStoreDataParams(createObjectDescription, strategyId, objectId, category); // Retrieve strategy data final StorageStrategy storageStrategy = STRATEGY_PROVIDER.getStorageStrategy(strategyId); final HotStrategy hotStrategy = storageStrategy.getHotStrategy(); if (hotStrategy != null) { // TODO: check this on starting application isStrategyValid(hotStrategy); final List<OfferReference> offerReferences = choosePriorityOffers(hotStrategy); if (offerReferences.isEmpty()) { throw new StorageNotFoundException(VitamCodeHelper.getLogMessage(VitamCode.STORAGE_OFFER_NOT_FOUND)); } TryAndRetryData datas = new TryAndRetryData(); datas.populateFromOfferReferences(offerReferences); StorageLogbookParameters parameters = tryAndRetry(objectId, createObjectDescription, category, requester, tenantId, datas, 1, null); logStorage(parameters); // TODO P1 Handle Status result if different for offers return buildStoreDataResponse(objectId, category, datas.getGlobalOfferResult()); } throw new StorageNotFoundException(VitamCodeHelper.getLogMessage(VitamCode.STORAGE_STRATEGY_NOT_FOUND)); } private StorageLogbookParameters tryAndRetry(String objectId, ObjectDescription createObjectDescription, DataCategory category, String requester, Integer tenantId, TryAndRetryData datas, int attempt, StorageLogbookParameters parameters) throws StorageTechnicalException, StorageNotFoundException, StorageObjectAlreadyExistsException { // init thread and make future map // Map here to keep offerId linked to Future Map<String, Future<ThreadResponseData>> futureMap = new HashMap<>(); int rank = 0; String offerId2 = null; Map<String, Digest> globalDigestMap = new HashMap<>(datas.getKoList().size()); long finalTimeout = 1000; try { for (final String offerId : datas.getKoList()) { Digest globalDigest = new Digest(digestType); globalDigestMap.put(offerId, globalDigest); Map<String, Object> streamAndInfos = getInputStreamFromWorkspace(createObjectDescription); InputStream digestInputStream = globalDigest.getDigestInputStream((InputStream) streamAndInfos.get(STREAM_KEY)); finalTimeout = getTransferTimeout(Long.valueOf((String) streamAndInfos.get(SIZE_KEY))); offerId2 = offerId; OfferReference offerReference = new OfferReference(); offerReference.setId(offerId); final Driver driver = retrieveDriverInternal(offerReference.getId()); StoragePutRequest request = new StoragePutRequest(tenantId, category.getFolder(), objectId, digestType.name(), digestInputStream); futureMap.put(offerReference.getId(), executor.submit(new TransferThread(driver, offerReference, request, globalDigest))); rank++; } } catch (NumberFormatException e) { LOGGER.error("Wrong number on wait on offer ID " + offerId2, e); parameters = setLogbookStorageParameters(parameters, offerId2, null, requester, attempt); } catch (StorageException e) { LOGGER.error("Interrupted on offer ID " + offerId2, e); parameters = setLogbookStorageParameters(parameters, offerId2, null, requester, attempt); } // wait all tasks submission try { Thread.sleep(threadSleep); } catch (InterruptedException exc) { LOGGER.warn("Thread sleep to wait all task submission interrupted !", exc); for (String offerId : futureMap.keySet()) { parameters = setLogbookStorageParameters(parameters, offerId, null, requester, attempt); } if (!datas.getKoList().isEmpty()) { try { deleteObjects(datas.getOkList(), tenantId, category, objectId, globalDigestMap); } catch (StorageTechnicalException e) { LOGGER.error("Cannot delete object {}", objectId, e); throw e; } } return parameters; } // wait for all threads execution // TODO: manage interruption and error execution (US #2008 && 2009) for (Entry<String, Future<ThreadResponseData>> entry : futureMap.entrySet()) { final Future<ThreadResponseData> future = entry.getValue(); String offerId = entry.getKey(); try { ThreadResponseData threadResponseData = future.get(finalTimeout, TimeUnit.MILLISECONDS); if (threadResponseData == null) { LOGGER.error("Error on offer ID " + offerId); parameters = setLogbookStorageParameters(parameters, offerId, null, requester, attempt); throw new StorageTechnicalException("No message returned"); } parameters = setLogbookStorageParameters(parameters, offerId, threadResponseData, requester, attempt); datas.koListToOkList(offerId); } catch (TimeoutException e) { LOGGER.info("Timeout on offer ID {} TimeOut: {}", offerId, finalTimeout, e); future.cancel(true); // TODO: manage thread to take into account this interruption LOGGER.error("Interrupted after timeout on offer ID " + offerId); parameters = setLogbookStorageParameters(parameters, offerId, null, requester, attempt); } catch (InterruptedException e) { LOGGER.error("Interrupted on offer ID " + offerId, e); parameters = setLogbookStorageParameters(parameters, offerId, null, requester, attempt); } catch (ExecutionException e) { LOGGER.error("Error on offer ID " + offerId, e); parameters = setLogbookStorageParameters(parameters, offerId, null, requester, attempt); // TODO: review this exception to manage errors correctly // Take into account Exception class // For example, for particular exception do not retry (because // it's useless) // US : #2009 } catch (NumberFormatException e) { future.cancel(true); LOGGER.error("Wrong number on wait on offer ID " + offerId, e); parameters = setLogbookStorageParameters(parameters, offerId, null, requester, attempt); } } // ACK to prevent retry if (attempt < NB_RETRY && !datas.getKoList().isEmpty()) { attempt++; tryAndRetry(objectId, createObjectDescription, category, requester, tenantId, datas, attempt, parameters); } // TODO : error management (US #2009) if (!datas.getKoList().isEmpty()) { deleteObjects(datas.getOkList(), tenantId, category, objectId, globalDigestMap); } return parameters; } private StorageLogbookParameters oldTryAndRetry(String objectId, ObjectDescription createObjectDescription, DataCategory category, String requester, Integer tenantId, TryAndRetryData datas, int attempt, StorageLogbookParameters parameters) throws StorageTechnicalException, StorageNotFoundException, StorageObjectAlreadyExistsException { Map<String, Object> streamAndInfos = getInputStreamFromWorkspace(createObjectDescription); Digest globalDigest = new Digest(digestType); InputStream digestInputStream = globalDigest.getDigestInputStream((InputStream) streamAndInfos.get(STREAM_KEY)); Digest digest = new Digest(digestType); try (MultipleInputStreamHandler streams = getMultipleInputStreamFromWorkspace(digestInputStream, datas.getKoList().size(), digest)) { // init thread and make future map // Map here to keep offerId linked to Future Map<String, Future<ThreadResponseData>> futureMap = new HashMap<>(); int rank = 0; String offerId2 = null; try { for (final String offerId : datas.getKoList()) { offerId2 = offerId; OfferReference offerReference = new OfferReference(); offerReference.setId(offerId); final Driver driver = retrieveDriverInternal(offerReference.getId()); StoragePutRequest request = new StoragePutRequest(tenantId, category.getFolder(), objectId, digestType.name(), streams.getInputStream(rank)); futureMap.put(offerReference.getId(), executor.submit(new TransferThread(driver, offerReference, request, globalDigest))); rank++; } } catch (NumberFormatException e) { LOGGER.error("Wrong number on wait on offer ID " + offerId2, e); parameters = setLogbookStorageParameters(parameters, offerId2, null, requester, attempt); } catch (StorageException e) { LOGGER.error("Interrupted on offer ID " + offerId2, e); parameters = setLogbookStorageParameters(parameters, offerId2, null, requester, attempt); } // wait all tasks submission try { Thread.sleep(threadSleep); } catch (InterruptedException exc) { LOGGER.warn("Thread sleep to wait all task submission interrupted !", exc); for (String offerId : futureMap.keySet()) { parameters = setLogbookStorageParameters(parameters, offerId, null, requester, attempt); } if (!datas.getKoList().isEmpty()) { try { oldDdeleteObjects(datas.getOkList(), tenantId, category, objectId, digest); } catch (StorageTechnicalException e) { LOGGER.error("Cannot delete object {}", objectId, e); throw e; } } return parameters; } // wait for all threads execution // TODO: manage interruption and error execution (US #2008 && 2009) for (Entry<String, Future<ThreadResponseData>> entry : futureMap.entrySet()) { final Future<ThreadResponseData> future = entry.getValue(); String offerId = entry.getKey(); try { ThreadResponseData threadResponseData = future .get(getTransferTimeout(Long.valueOf((String) streamAndInfos.get(SIZE_KEY))), TimeUnit.MILLISECONDS); if (threadResponseData == null) { LOGGER.error("Error on offer ID " + offerId); parameters = setLogbookStorageParameters(parameters, offerId, null, requester, attempt); throw new StorageTechnicalException("No message returned"); } parameters = setLogbookStorageParameters(parameters, offerId, threadResponseData, requester, attempt); datas.koListToOkList(offerId); } catch (TimeoutException e) { LOGGER.info("Timeout on offer ID {} TimeOut: {}", offerId, getTransferTimeout(Long.valueOf((String) streamAndInfos.get(SIZE_KEY))), e); future.cancel(true); // TODO: manage thread to take into account this interruption LOGGER.error("Interrupted after timeout on offer ID " + offerId); parameters = setLogbookStorageParameters(parameters, offerId, null, requester, attempt); } catch (InterruptedException e) { LOGGER.error("Interrupted on offer ID " + offerId, e); parameters = setLogbookStorageParameters(parameters, offerId, null, requester, attempt); } catch (ExecutionException e) { LOGGER.error("Error on offer ID " + offerId, e); parameters = setLogbookStorageParameters(parameters, offerId, null, requester, attempt); // TODO: review this exception to manage errors correctly // Take into account Exception class // For example, for particular exception do not retry (because // it's useless) // US : #2009 } catch (NumberFormatException e) { future.cancel(true); LOGGER.error("Wrong number on wait on offer ID " + offerId, e); parameters = setLogbookStorageParameters(parameters, offerId, null, requester, attempt); } } } // ACK to prevent retry if (attempt < NB_RETRY && !datas.getKoList().isEmpty()) { attempt++; tryAndRetry(objectId, createObjectDescription, category, requester, tenantId, datas, attempt, parameters); } // TODO : error management (US #2009) if (!datas.getKoList().isEmpty()) { oldDdeleteObjects(datas.getOkList(), tenantId, category, objectId, digest); } return parameters; } private long getTransferTimeout(long sizeToTransfer) { long timeout = (sizeToTransfer / 1024) * millisecondsPerKB; if (timeout < DEFAULT_MINIMUM_TIMEOUT) { return DEFAULT_MINIMUM_TIMEOUT; } return timeout; } private void isStrategyValid(HotStrategy hotStrategy) throws StorageTechnicalException { if (!hotStrategy.isCopyValid()) { throw new StorageTechnicalException("Invalid number of copy"); } } private Map<String, Object> getInputStreamFromWorkspace(ObjectDescription createObjectDescription) throws StorageTechnicalException, StorageNotFoundException { try (WorkspaceClient workspaceClient = mockedWorkspaceClient == null ? WorkspaceClientFactory.getInstance().getClient() : mockedWorkspaceClient) { return retrieveDataFromWorkspace(createObjectDescription.getWorkspaceContainerGUID(), createObjectDescription.getWorkspaceObjectURI(), workspaceClient); } } private MultipleInputStreamHandler getMultipleInputStreamFromWorkspace(InputStream stream, int nbCopy, Digest digest) throws StorageTechnicalException, StorageNotFoundException { DigestInputStream digestOriginalStream = (DigestInputStream) digest.getDigestInputStream(stream); return new MultipleInputStreamHandler(digestOriginalStream, nbCopy); } private StorageLogbookParameters setLogbookStorageParameters(StorageLogbookParameters parameters, String offerId, ThreadResponseData res, String requester, int attempt) { if (parameters == null) { parameters = getParameters(res != null ? res.getObjectGuid() : null, res != null ? res.getResponse() : null, null, offerId, res != null ? res.getStatus() : Status.INTERNAL_SERVER_ERROR, requester, attempt); } else { updateStorageLogbookParameters(parameters, offerId, res != null ? res.getStatus() : Status.INTERNAL_SERVER_ERROR, attempt); } return parameters; } private void logStorage(StorageLogbookParameters parameters) throws StorageTechnicalException { try { final StorageLogbook storageLogbook = StorageLogbookFactory.getInstance().getStorageLogbook(); storageLogbook.add(parameters); } catch (final StorageException exc) { throw new StorageTechnicalException(VitamCodeHelper.getLogMessage(VitamCode.STORAGE_LOGBOOK_CANNOT_LOG), exc); } } private StoredInfoResult buildStoreDataResponse(String objectId, DataCategory category, Map<String, Status> offerResults) throws StorageTechnicalException { final String offerIds = String.join(", ", offerResults.keySet()); // Aggregate result of all store actions. If all went well, allSuccess // is true, false if one action failed final boolean allSuccess = offerResults.entrySet().stream().map(Map.Entry::getValue) .noneMatch(Status.INTERNAL_SERVER_ERROR::equals); if (!allSuccess) { LOGGER.error(VitamCodeHelper.getLogMessage(VitamCode.STORAGE_CANT_STORE_OBJECT, objectId, offerIds)); throw new StorageTechnicalException( VitamCodeHelper.getLogMessage(VitamCode.STORAGE_CANT_STORE_OBJECT, objectId, offerIds)); } // TODO P1 Witch status code return if an offer is updated (Status.OK) // and another is created (Status.CREATED) ? final StoredInfoResult result = new StoredInfoResult(); final LocalDateTime now = LocalDateTime.now(); final StringBuilder description = new StringBuilder(); switch (category) { case UNIT: description.append("Unit "); break; case OBJECT_GROUP: description.append("ObjectGroup "); break; case LOGBOOK: description.append("Logbook "); break; case OBJECT: description.append("Object "); break; case REPORT: description.append("Report "); break; case MANIFEST: description.append("Manifest "); break; default: throw new UnsupportedOperationException(NOT_IMPLEMENTED_MSG); } description.append("with id '"); description.append(objectId); description.append("' stored successfully"); result.setId(objectId); result.setInfo(description.toString()); result.setCreationTime(LocalDateUtil.getString(now)); result.setLastAccessTime(LocalDateUtil.getString(now)); result.setLastCheckedTime(LocalDateUtil.getString(now)); result.setLastModifiedTime(LocalDateUtil.getString(now)); return result; } /** * Storage logbook entry for ONE offer * * @param objectGuid the object Guid * @param putObjectResult the response * @param messageDigest the computed digest * @param offerId the offerId * @param objectStored the operation status * @return storage logbook parameters */ private StorageLogbookParameters getParameters(String objectGuid, StoragePutResult putObjectResult, Digest messageDigest, String offerId, Status objectStored, String requester, int attempt) { final String objectIdentifier = objectGuid != null ? objectGuid : "objectRequest NA"; final String messageDig = messageDigest != null ? messageDigest.digestHex() : "messageDigest NA"; final String size = putObjectResult != null ? String.valueOf(putObjectResult.getObjectSize()) : "Size NA"; boolean error = objectStored == Status.INTERNAL_SERVER_ERROR; final StorageLogbookOutcome outcome = error ? StorageLogbookOutcome.KO : StorageLogbookOutcome.OK; return getStorageLogbookParameters(objectIdentifier, null, messageDig, digestType.getName(), size, getAttemptLog(offerId, attempt, error), requester, null, null, outcome); } private String getAttemptLog(String offerId, int attempt, boolean error) { StringBuilder sb = new StringBuilder(); sb.append(offerId).append(" attempt ").append(attempt).append(" : ").append(error ? "KO" : "OK"); return sb.toString(); } private void updateStorageLogbookParameters(StorageLogbookParameters parameters, String offerId, Status status, int attempt) { String offers = parameters.getMapParameters().get(StorageLogbookParameterName.agentIdentifiers); if (Status.INTERNAL_SERVER_ERROR.equals(status)) { parameters.getMapParameters().put(StorageLogbookParameterName.outcome, StorageLogbookOutcome.KO.name()); offers += ", " + offerId + " attempt " + attempt + " : KO"; } else { offers += ", " + offerId + " attempt " + attempt + " : OK"; parameters.setStatus(StorageLogbookOutcome.OK); } parameters.getMapParameters().put(StorageLogbookParameterName.agentIdentifiers, offers); } private Driver retrieveDriverInternal(String offerId) throws StorageTechnicalException { try { return DriverManager.getDriverFor(offerId); } catch (final StorageDriverNotFoundException exc) { throw new StorageTechnicalException(exc); } } private void checkStoreDataParams(ObjectDescription createObjectDescription, String strategyId, String dataId, DataCategory category) { ParametersChecker.checkParameter(STRATEGY_ID_IS_MANDATORY, strategyId); ParametersChecker.checkParameter("Object id is mandatory", dataId); ParametersChecker.checkParameter("Category is mandatory", category); ParametersChecker.checkParameter("Object additional information guid is mandatory", createObjectDescription); ParametersChecker .checkParameter("Container guid is mandatory", createObjectDescription.getWorkspaceContainerGUID()); ParametersChecker .checkParameter("Object URI in workspaceis mandatory", createObjectDescription.getWorkspaceObjectURI()); } private Map<String, Object> retrieveDataFromWorkspace(String containerGUID, String objectURI, WorkspaceClient workspaceClient) throws StorageNotFoundException, StorageTechnicalException { Response response = null; try { response = workspaceClient.getObject(containerGUID, objectURI); Map<String, Object> result = new HashMap<>(); String length = response.getHeaderString(VitamHttpHeader.X_CONTENT_LENGTH.getName()); Object entity = response.getEntity(); if (entity == null) { LOGGER.error(VitamCodeHelper.getLogMessage(VitamCode.STORAGE_OBJECT_NOT_FOUND, containerGUID)); throw new StorageNotFoundException( VitamCodeHelper.getLogMessage(VitamCode.STORAGE_OBJECT_NOT_FOUND, containerGUID)); } try { ParametersChecker.checkParameter("Lenght is empty", length); // TODO: why? Long.valueOf(length); } catch (IllegalArgumentException e) { // Default value (hack) LOGGER.warn("no Length returned", e); length = DEFAULT_SIZE_WHEN_UNKNOWN; } result.put(SIZE_KEY, length); result.put(STREAM_KEY, entity); return result; } catch (final ContentAddressableStorageNotFoundException exc) { LOGGER.error(VitamCodeHelper.getLogMessage(VitamCode.STORAGE_OBJECT_NOT_FOUND, containerGUID), exc); throw new StorageNotFoundException(exc); } catch (final ContentAddressableStorageServerException exc) { LOGGER.error(VitamCodeHelper.getLogMessage(VitamCode.STORAGE_TECHNICAL_INTERNAL_ERROR), exc); throw new StorageTechnicalException(exc); } } @Override public JsonNode getContainerInformation(String strategyId) throws StorageException { Integer tenantId = ParameterHelper.getTenantParameter(); ParametersChecker.checkParameter(STRATEGY_ID_IS_MANDATORY, strategyId); // Retrieve strategy data final StorageStrategy storageStrategy = STRATEGY_PROVIDER.getStorageStrategy(strategyId); final HotStrategy hotStrategy = storageStrategy.getHotStrategy(); if (hotStrategy != null) { // TODO: check this on starting application isStrategyValid(hotStrategy); final List<OfferReference> offerReferences = choosePriorityOffers(hotStrategy); if (offerReferences.isEmpty()) { LOGGER.error(VitamCodeHelper.getLogMessage(VitamCode.STORAGE_OFFER_NOT_FOUND)); throw new StorageNotFoundException(VitamCodeHelper.getLogMessage(VitamCode.STORAGE_OFFER_NOT_FOUND)); } ArrayNode resultArray = JsonHandler.createArrayNode(); for (OfferReference offerReference : offerReferences) { resultArray.add(getOfferInformation(offerReference, tenantId)); } return JsonHandler.createObjectNode().set("capacities", resultArray); } LOGGER.error(VitamCodeHelper.getLogMessage(VitamCode.STORAGE_STRATEGY_NOT_FOUND)); throw new StorageNotFoundException(VitamCodeHelper.getLogMessage(VitamCode.STORAGE_STRATEGY_NOT_FOUND)); } private JsonNode getOfferInformation(OfferReference offerReference, Integer tenantId) throws StorageException { final Driver driver = retrieveDriverInternal(offerReference.getId()); final StorageOffer offer = OFFER_PROVIDER.getStorageOffer(offerReference.getId()); final Properties parameters = new Properties(); parameters.putAll(offer.getParameters()); try (Connection connection = driver.connect(offer, parameters)) { final ObjectNode ret = JsonHandler.createObjectNode(); ret.put("offerId", offer.getId()); ret.put("usableSpace", connection.getStorageCapacity(tenantId).getUsableSpace()); return ret; } catch (StorageDriverException | RuntimeException exc) { // TODO IT_13 (celeg): response error ? (like offerId + usableSpace // to 0 ?) LOGGER.error(VitamCodeHelper.getLogMessage(VitamCode.STORAGE_TECHNICAL_INTERNAL_ERROR), exc); throw new StorageTechnicalException(exc); } } @Override public InputStream getStorageContainer(String strategyId) throws StorageNotFoundException, StorageTechnicalException { LOGGER.error(NOT_IMPLEMENTED_MSG); throw new UnsupportedOperationException(NOT_IMPLEMENTED_MSG); } private List<OfferReference> choosePriorityOffers(HotStrategy hotStrategy) { final List<OfferReference> offerReferences = new ArrayList<>(); if (hotStrategy != null && !hotStrategy.getOffers().isEmpty()) { // TODO P1 : this code will be changed in the future to handle // priority (not in current US scope) and copy offerReferences.addAll(hotStrategy.getOffers()); } return offerReferences; } private StorageLogbookParameters getStorageLogbookParameters(String objectIdentifier, GUID objectGroupIdentifier, String digest, String digestAlgorithm, String size, String agentIdentifiers, String agentIdentifierRequester, String outcomeDetailMessage, String objectIdentifierIncome, StorageLogbookOutcome outcome) { final Map<StorageLogbookParameterName, String> mandatoryParameters = new TreeMap<>(); mandatoryParameters.put(StorageLogbookParameterName.eventDateTime, LocalDateUtil.now().toString()); mandatoryParameters.put(StorageLogbookParameterName.outcome, outcome.name()); mandatoryParameters.put(StorageLogbookParameterName.objectIdentifier, objectIdentifier != null ? objectIdentifier : "objId NA"); mandatoryParameters.put(StorageLogbookParameterName.objectGroupIdentifier, objectGroupIdentifier != null ? objectGroupIdentifier.toString() : "objGId NA"); mandatoryParameters.put(StorageLogbookParameterName.digest, digest); mandatoryParameters.put(StorageLogbookParameterName.digestAlgorithm, digestAlgorithm); mandatoryParameters.put(StorageLogbookParameterName.size, size); mandatoryParameters.put(StorageLogbookParameterName.agentIdentifiers, agentIdentifiers); mandatoryParameters.put(StorageLogbookParameterName.agentIdentifierRequester, agentIdentifierRequester); final StorageLogbookParameters parameters = new StorageLogbookParameters(mandatoryParameters); if (outcomeDetailMessage != null) { parameters.setOutcomDetailMessage(outcomeDetailMessage); } if (objectIdentifierIncome != null) { parameters.setObjectIdentifierIncome(objectIdentifierIncome); } return parameters; } @Override public JsonNode createContainer(String strategyId) throws StorageException { LOGGER.error(NOT_IMPLEMENTED_MSG); throw new UnsupportedOperationException(NOT_IMPLEMENTED_MSG); } @Override public void deleteContainer(String strategyId) throws StorageTechnicalException, StorageNotFoundException { LOGGER.error(NOT_IMPLEMENTED_MSG); throw new UnsupportedOperationException(NOT_IMPLEMENTED_MSG); } @Override public Response listContainerObjects(String strategyId, DataCategory category, String cursorId) throws StorageException { Integer tenantId = ParameterHelper.getTenantParameter(); ParametersChecker.checkParameter(STRATEGY_ID_IS_MANDATORY, strategyId); ParametersChecker.checkParameter(CATEGORY_IS_MANDATORY, category); final StorageStrategy storageStrategy = STRATEGY_PROVIDER.getStorageStrategy(strategyId); final HotStrategy hotStrategy = storageStrategy.getHotStrategy(); if (hotStrategy != null) { final List<OfferReference> offerReferences = choosePriorityOffers(hotStrategy); if (offerReferences.isEmpty()) { LOGGER.error(VitamCodeHelper.getLogMessage(VitamCode.STORAGE_OFFER_NOT_FOUND)); throw new StorageTechnicalException(VitamCodeHelper.getLogMessage(VitamCode.STORAGE_OFFER_NOT_FOUND)); } // TODO: make priority -> Use the first one here but don't take into // account errors ! final StorageOffer offer = OFFER_PROVIDER.getStorageOffer(offerReferences.get(0).getId()); final Driver driver = retrieveDriverInternal(offerReferences.get(0).getId()); final Properties parameters = new Properties(); parameters.putAll(offer.getParameters()); try (Connection connection = driver.connect(offer, parameters)) { StorageListRequest request = new StorageListRequest(tenantId, category.getFolder(), cursorId, true); return connection.listObjects(request); } catch (final StorageDriverException exc) { LOGGER.error(VitamCodeHelper.getLogMessage(VitamCode.STORAGE_TECHNICAL_INTERNAL_ERROR), exc); throw new StorageTechnicalException(exc); } } LOGGER.error(VitamCodeHelper.getLogMessage(VitamCode.STORAGE_STRATEGY_NOT_FOUND)); throw new StorageNotFoundException(VitamCodeHelper.getLogMessage(VitamCode.STORAGE_STRATEGY_NOT_FOUND)); } @Override public Response getContainerByCategory(String strategyId, String objectId, DataCategory category, AsyncResponse asyncResponse) throws StorageException { // Check input params Integer tenantId = ParameterHelper.getTenantParameter(); ParametersChecker.checkParameter(STRATEGY_ID_IS_MANDATORY, strategyId); ParametersChecker.checkParameter("Object id is mandatory", objectId); // Retrieve strategy data final StorageStrategy storageStrategy = STRATEGY_PROVIDER.getStorageStrategy(strategyId); final HotStrategy hotStrategy = storageStrategy.getHotStrategy(); if (hotStrategy != null) { // TODO: check this on starting application isStrategyValid(hotStrategy); final List<OfferReference> offerReferences = choosePriorityOffers(hotStrategy); if (offerReferences.isEmpty()) { LOGGER.error(VitamCodeHelper.getLogMessage(VitamCode.STORAGE_TECHNICAL_INTERNAL_ERROR)); throw new StorageTechnicalException(VitamCodeHelper.getLogMessage(VitamCode.STORAGE_OFFER_NOT_FOUND)); } final StorageGetResult result = getGetObjectResult(tenantId, objectId, category, offerReferences, asyncResponse); return result.getObject(); } LOGGER.error(VitamCodeHelper.getLogMessage(VitamCode.STORAGE_STRATEGY_NOT_FOUND)); throw new StorageTechnicalException(VitamCodeHelper.getLogMessage(VitamCode.STORAGE_STRATEGY_NOT_FOUND)); } private StorageGetResult getGetObjectResult(Integer tenantId, String objectId, DataCategory type, List<OfferReference> offerReferences, AsyncResponse asyncResponse) throws StorageException { StorageGetResult result; for (final OfferReference offerReference : offerReferences) { final Driver driver = retrieveDriverInternal(offerReference.getId()); final StorageOffer offer = OFFER_PROVIDER.getStorageOffer(offerReference.getId()); final Properties parameters = new Properties(); parameters.putAll(offer.getParameters()); try (Connection connection = driver.connect(offer, parameters)) { final StorageObjectRequest request = new StorageObjectRequest(tenantId, type.getFolder(), objectId); result = connection.getObject(request); if (result.getObject() != null) { final AsyncInputStreamHelper helper = new AsyncInputStreamHelper(asyncResponse, result.getObject()); final ResponseBuilder responseBuilder = Response.status(Status.OK).type(MediaType.APPLICATION_OCTET_STREAM); helper.writeResponse(responseBuilder); return result; } } catch (final StorageDriverException exc) { LOGGER.warn("Error with the storage, take the next offer in the strategy (by priority)", exc); } } LOGGER.error(VitamCodeHelper.getLogMessage(VitamCode.STORAGE_OBJECT_NOT_FOUND), objectId); throw new StorageNotFoundException(VitamCodeHelper.getLogMessage(VitamCode.STORAGE_OBJECT_NOT_FOUND, objectId)); } @Override public JsonNode getContainerObjectInformations(String strategyId, String objectId) throws StorageNotFoundException { LOGGER.error(NOT_IMPLEMENTED_MSG); throw new UnsupportedOperationException(NOT_IMPLEMENTED_MSG); } private void deleteObjects(List<String> offerIdList, Integer tenantId, DataCategory category, String objectId, Map<String, Digest> digests) throws StorageTechnicalException { // Map here to keep offerId linked to Future Map<String, Future<Boolean>> futureMap = new HashMap<>(); for (String offerId : offerIdList) { final Driver driver = retrieveDriverInternal(offerId); // TODO: review if digest value is really good ? StorageRemoveRequest request = new StorageRemoveRequest(tenantId, category.getFolder(), objectId, digestType, digests.get(offerId).digestHex()); futureMap.put(offerId, executor.submit(new DeleteThread(driver, request, offerId))); } // wait all tasks submission try { Thread.sleep(threadSleep, TimeUnit.MILLISECONDS.ordinal()); } catch (InterruptedException exc) { LOGGER.warn("Thread sleep to wait all task submission interrupted !", exc); throw new StorageTechnicalException("Object potentially not deleted: " + objectId, exc); } for (Entry<String, Future<Boolean>> entry : futureMap.entrySet()) { final Future<Boolean> future = entry.getValue(); String offerId = entry.getKey(); try { Boolean bool = future.get(DEFAULT_MINIMUM_TIMEOUT * 10, TimeUnit.MILLISECONDS); if (!bool) { LOGGER.error("Object not deleted: {}", objectId); throw new StorageTechnicalException("Object not deleted: " + objectId); } } catch (TimeoutException e) { LOGGER.error("Timeout on offer ID " + offerId, e); future.cancel(true); // TODO: manage thread to take into account this interruption LOGGER.error("Interrupted after timeout on offer ID " + offerId, e); throw new StorageTechnicalException("Object not deleted: " + objectId); } catch (InterruptedException e) { LOGGER.error("Interrupted on offer ID " + offerId, e); throw new StorageTechnicalException("Object not deleted: " + objectId, e); } catch (ExecutionException e) { LOGGER.error("Error on offer ID " + offerId, e); // TODO: review this exception to manage errors correctly // Take into account Exception class // For example, for particular exception do not retry (because // it's useless) // US : #2009 throw new StorageTechnicalException("Object not deleted: " + objectId, e); } catch (NumberFormatException e) { future.cancel(true); LOGGER.error("Wrong number on wait on offer ID " + offerId, e); throw new StorageTechnicalException("Object not deleted: " + objectId, e); } } } private void oldDdeleteObjects(List<String> offerIdList, Integer tenantId, DataCategory category, String objectId, Digest digest) throws StorageTechnicalException { // Map here to keep offerId linked to Future Map<String, Future<Boolean>> futureMap = new HashMap<>(); for (String offerId : offerIdList) { final Driver driver = retrieveDriverInternal(offerId); // TODO: review if digest value is really good ? StorageRemoveRequest request = new StorageRemoveRequest(tenantId, category.getFolder(), objectId, digestType, digest.digestHex()); futureMap.put(offerId, executor.submit(new DeleteThread(driver, request, offerId))); } // wait all tasks submission try { Thread.sleep(threadSleep, TimeUnit.MILLISECONDS.ordinal()); } catch (InterruptedException exc) { LOGGER.warn("Thread sleep to wait all task submission interrupted !", exc); throw new StorageTechnicalException("Object potentially not deleted: " + objectId, exc); } for (Entry<String, Future<Boolean>> entry : futureMap.entrySet()) { final Future<Boolean> future = entry.getValue(); String offerId = entry.getKey(); try { Boolean bool = future.get(DEFAULT_MINIMUM_TIMEOUT * 10, TimeUnit.MILLISECONDS); if (!bool) { LOGGER.error("Object not deleted: {}", objectId); throw new StorageTechnicalException("Object not deleted: " + objectId); } } catch (TimeoutException e) { LOGGER.error("Timeout on offer ID " + offerId, e); future.cancel(true); // TODO: manage thread to take into account this interruption LOGGER.error("Interrupted after timeout on offer ID " + offerId, e); throw new StorageTechnicalException("Object not deleted: " + objectId); } catch (InterruptedException e) { LOGGER.error("Interrupted on offer ID " + offerId, e); throw new StorageTechnicalException("Object not deleted: " + objectId, e); } catch (ExecutionException e) { LOGGER.error("Error on offer ID " + offerId, e); // TODO: review this exception to manage errors correctly // Take into account Exception class // For example, for particular exception do not retry (because // it's useless) // US : #2009 throw new StorageTechnicalException("Object not deleted: " + objectId, e); } catch (NumberFormatException e) { future.cancel(true); LOGGER.error("Wrong number on wait on offer ID " + offerId, e); throw new StorageTechnicalException("Object not deleted: " + objectId, e); } } } @Override public void deleteObject(String strategyId, String objectId, String digest, DigestType digestAlgorithm) throws StorageException { // Check input params Integer tenantId = ParameterHelper.getTenantParameter(); ParametersChecker.checkParameter(STRATEGY_ID_IS_MANDATORY, strategyId); ParametersChecker.checkParameter("Object id is mandatory", objectId); ParametersChecker.checkParameter("Digest is mandatory", digest); ParametersChecker.checkParameter("Digest Algorithm is mandatory", digestAlgorithm); // Retrieve strategy data final StorageStrategy storageStrategy = STRATEGY_PROVIDER.getStorageStrategy(strategyId); final HotStrategy hotStrategy = storageStrategy.getHotStrategy(); if (hotStrategy != null) { // TODO: check this on starting application isStrategyValid(hotStrategy); final List<OfferReference> offerReferences = choosePriorityOffers(hotStrategy); if (offerReferences.isEmpty()) { LOGGER.error(VitamCodeHelper.getLogMessage(VitamCode.STORAGE_OFFER_NOT_FOUND)); throw new StorageTechnicalException(VitamCodeHelper.getLogMessage(VitamCode.STORAGE_OFFER_NOT_FOUND)); } // TODO : Improve this code, use same thread system as used for the // storeData method see @TrasferThread for (final OfferReference offerReference : offerReferences) { final Driver driver = retrieveDriverInternal(offerReference.getId()); final StorageOffer offer = OFFER_PROVIDER.getStorageOffer(offerReference.getId()); final Properties parameters = new Properties(); parameters.putAll(offer.getParameters()); try (Connection connection = driver.connect(offer, parameters)) { StorageRemoveRequest request = new StorageRemoveRequest(tenantId, DataCategory.OBJECT.getFolder(), objectId, digestType, digest); StorageRemoveResult result = connection.removeObject(request); if (!result.isObjectDeleted()) { LOGGER.error(VitamCodeHelper.getLogMessage(VitamCode.STORAGE_OBJECT_NOT_FOUND)); throw new StorageTechnicalException("Object not deleted: " + objectId); } } catch (StorageDriverException exc) { LOGGER.error(VitamCodeHelper.getLogMessage(VitamCode.STORAGE_TECHNICAL_INTERNAL_ERROR), exc); if (exc.getErrorCode() == StorageDriverException.ErrorCode.NOT_FOUND) { throw new StorageTechnicalException( VitamCodeHelper.getLogMessage(VitamCode.STORAGE_OFFER_NOT_FOUND)); } throw new StorageTechnicalException(exc); } catch (RuntimeException exc) { throw new StorageTechnicalException(exc); } } } } @Override public JsonNode getContainerLogbooks(String strategyId) throws StorageNotFoundException { LOGGER.error(NOT_IMPLEMENTED_MSG); throw new UnsupportedOperationException(NOT_IMPLEMENTED_MSG); } @Override public JsonNode getContainerLogbook(String strategyId, String logbookId) throws StorageNotFoundException { LOGGER.error(NOT_IMPLEMENTED_MSG); throw new UnsupportedOperationException(NOT_IMPLEMENTED_MSG); } @Override public void deleteLogbook(String strategyId, String logbookId) throws StorageNotFoundException { LOGGER.error(NOT_IMPLEMENTED_MSG); throw new UnsupportedOperationException(NOT_IMPLEMENTED_MSG); } @Override public JsonNode getContainerUnits(String strategyId) throws StorageNotFoundException { LOGGER.error(NOT_IMPLEMENTED_MSG); throw new UnsupportedOperationException(NOT_IMPLEMENTED_MSG); } @Override public JsonNode getContainerUnit(String strategyId, String unitId) throws StorageNotFoundException { LOGGER.error(NOT_IMPLEMENTED_MSG); throw new UnsupportedOperationException(NOT_IMPLEMENTED_MSG); } @Override public void deleteUnit(String strategyId, String unitId) throws StorageNotFoundException { LOGGER.error(NOT_IMPLEMENTED_MSG); throw new UnsupportedOperationException(NOT_IMPLEMENTED_MSG); } @Override public JsonNode getContainerObjectGroups(String strategyId) throws StorageNotFoundException { LOGGER.error(NOT_IMPLEMENTED_MSG); throw new UnsupportedOperationException(NOT_IMPLEMENTED_MSG); } @Override public JsonNode getContainerObjectGroup(String strategyId, String objectGroupId) throws StorageNotFoundException { LOGGER.error(NOT_IMPLEMENTED_MSG); throw new UnsupportedOperationException(NOT_IMPLEMENTED_MSG); } @Override public void deleteObjectGroup(String strategyId, String objectGroupId) throws StorageNotFoundException { LOGGER.error(NOT_IMPLEMENTED_MSG); throw new UnsupportedOperationException(NOT_IMPLEMENTED_MSG); } @Override public JsonNode status() throws StorageException { LOGGER.error(NOT_IMPLEMENTED_MSG); throw new UnsupportedOperationException(NOT_IMPLEMENTED_MSG); } @Override public void close() { executor.shutdown(); try { executor.awaitTermination(10000, TimeUnit.MILLISECONDS); } catch (InterruptedException e) { LOGGER.warn(e); } executor.shutdownNow(); } }