/* * (C) Copyright 2015 Nuxeo SA (http://nuxeo.com/) and others. * * 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. * * Contributors: * Thierry Delprat <tdelprat@nuxeo.com> * Antoine Taillefer <ataillefer@nuxeo.com> * */ package org.nuxeo.ecm.automation.server.jaxrs.batch; import java.io.IOException; import java.io.InputStream; import java.io.Serializable; import java.security.Principal; import java.util.Collections; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.UUID; import java.util.concurrent.atomic.AtomicInteger; import org.apache.commons.lang.StringUtils; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import org.nuxeo.ecm.automation.AutomationService; import org.nuxeo.ecm.automation.OperationContext; import org.nuxeo.ecm.automation.OperationException; import org.nuxeo.ecm.automation.core.util.BlobList; import org.nuxeo.ecm.automation.core.util.ComplexTypeJSONDecoder; import org.nuxeo.ecm.automation.server.AutomationServer; import org.nuxeo.ecm.automation.server.RestBinding; import org.nuxeo.ecm.core.api.Blob; import org.nuxeo.ecm.core.api.CoreSession; import org.nuxeo.ecm.core.api.NuxeoException; import org.nuxeo.ecm.core.api.NuxeoPrincipal; import org.nuxeo.ecm.core.transientstore.api.TransientStore; import org.nuxeo.ecm.core.transientstore.api.TransientStoreService; import org.nuxeo.ecm.webengine.model.exceptions.WebSecurityException; import org.nuxeo.runtime.api.Framework; import org.nuxeo.runtime.model.DefaultComponent; import org.nuxeo.runtime.services.config.ConfigurationService; /** * Runtime Component implementing the {@link BatchManager} service with the {@link TransientStore}. * * @since 5.4.2 */ public class BatchManagerComponent extends DefaultComponent implements BatchManager { protected static final Log log = LogFactory.getLog(BatchManagerComponent.class); protected static final String TRANSIENT_STORE_NAME = "BatchManagerCache"; protected static final String CLIENT_BATCH_ID_FLAG = "allowClientGeneratedBatchId"; protected final AtomicInteger uploadInProgress = new AtomicInteger(0); static { ComplexTypeJSONDecoder.registerBlobDecoder(new JSONBatchBlobDecoder()); } @Override public TransientStore getTransientStore() { TransientStoreService tss = Framework.getService(TransientStoreService.class); return tss.getStore(TRANSIENT_STORE_NAME); } @Override public String initBatch() { Batch batch = initBatchInternal(null); return batch.getKey(); } @Override @Deprecated public String initBatch(String batchId, String contextName) { Batch batch = initBatchInternal(batchId); return batch.getKey(); } protected Batch initBatchInternal(String batchId) { if (StringUtils.isEmpty(batchId)) { batchId = "batchId-" + UUID.randomUUID().toString(); } else if (!Framework.getService(ConfigurationService.class).isBooleanPropertyTrue(CLIENT_BATCH_ID_FLAG)) { throw new NuxeoException(String.format( "Cannot initialize upload batch with a given id since configuration property %s is not set to true", CLIENT_BATCH_ID_FLAG)); } // That's the way of storing an empty entry log.debug("Initializing batch with id " + batchId); getTransientStore().setCompleted(batchId, false); return new Batch(batchId); } public Batch getBatch(String batchId) { Map<String, Serializable> batchEntryParams = getTransientStore().getParameters(batchId); if (batchEntryParams == null) { if (!hasBatch(batchId)) { return null; } batchEntryParams = new HashMap<>(); } return new Batch(batchId, batchEntryParams); } @Override public void addStream(String batchId, String index, InputStream is, String name, String mime) throws IOException { uploadInProgress.incrementAndGet(); try { Batch batch = getBatch(batchId); if (batch == null) { batch = initBatchInternal(batchId); } batch.addFile(index, is, name, mime); log.debug(String.format("Added file %s [%s] to batch %s", index, name, batch.getKey())); } finally { uploadInProgress.decrementAndGet(); } } @Override public void addStream(String batchId, String index, InputStream is, int chunkCount, int chunkIndex, String name, String mime, long fileSize) throws IOException { uploadInProgress.incrementAndGet(); try { Batch batch = getBatch(batchId); if (batch == null) { batch = initBatchInternal(batchId); } batch.addChunk(index, is, chunkCount, chunkIndex, name, mime, fileSize); log.debug(String.format("Added chunk %s to file %s [%s] in batch %s", chunkIndex, index, name, batch.getKey())); } finally { uploadInProgress.decrementAndGet(); } } @Override public boolean hasBatch(String batchId) { return batchId != null && getTransientStore().exists(batchId); } @Override public List<Blob> getBlobs(String batchId) { return getBlobs(batchId, 0); } @Override public List<Blob> getBlobs(String batchId, int timeoutS) { if (uploadInProgress.get() > 0 && timeoutS > 0) { for (int i = 0; i < timeoutS * 5; i++) { try { Thread.sleep(200); } catch (InterruptedException e) { Thread.currentThread().interrupt(); } if (uploadInProgress.get() == 0) { break; } } } Batch batch = getBatch(batchId); if (batch == null) { log.error("Unable to find batch with id " + batchId); return Collections.emptyList(); } return batch.getBlobs(); } @Override public Blob getBlob(String batchId, String fileIndex) { return getBlob(batchId, fileIndex, 0); } @Override public Blob getBlob(String batchId, String fileIndex, int timeoutS) { Blob blob = getBatchBlob(batchId, fileIndex); if (blob == null && timeoutS > 0 && uploadInProgress.get() > 0) { for (int i = 0; i < timeoutS * 5; i++) { try { Thread.sleep(200); } catch (InterruptedException e) { Thread.currentThread().interrupt(); } blob = getBatchBlob(batchId, fileIndex); if (blob != null) { break; } } } if (!hasBatch(batchId)) { log.error("Unable to find batch with id " + batchId); return null; } return blob; } protected Blob getBatchBlob(String batchId, String fileIndex) { Blob blob = null; Batch batch = getBatch(batchId); if (batch != null) { blob = batch.getBlob(fileIndex); } return blob; } @Override public List<BatchFileEntry> getFileEntries(String batchId) { Batch batch = getBatch(batchId); if (batch == null) { return null; } return batch.getFileEntries(); } @Override public BatchFileEntry getFileEntry(String batchId, String fileIndex) { Batch batch = getBatch(batchId); if (batch == null) { return null; } return batch.getFileEntry(fileIndex); } @Override public void clean(String batchId) { Batch batch = getBatch(batchId); if (batch != null) { batch.clean(); } } @Override public Object execute(String batchId, String chainOrOperationId, CoreSession session, Map<String, Object> contextParams, Map<String, Object> operationParams) { List<Blob> blobs = getBlobs(batchId, getUploadWaitTimeout()); if (blobs == null) { String message = String.format("Unable to find batch associated with id '%s'", batchId); log.error(message); throw new NuxeoException(message); } return execute(new BlobList(blobs), chainOrOperationId, session, contextParams, operationParams); } @Override public Object execute(String batchId, String fileIndex, String chainOrOperationId, CoreSession session, Map<String, Object> contextParams, Map<String, Object> operationParams) { Blob blob = getBlob(batchId, fileIndex, getUploadWaitTimeout()); if (blob == null) { String message = String.format( "Unable to find batch associated with id '%s' or file associated with index '%s'", batchId, fileIndex); log.error(message); throw new NuxeoException(message); } return execute(blob, chainOrOperationId, session, contextParams, operationParams); } protected Object execute(Object blobInput, String chainOrOperationId, CoreSession session, Map<String, Object> contextParams, Map<String, Object> operationParams) { if (contextParams == null) { contextParams = new HashMap<>(); } if (operationParams == null) { operationParams = new HashMap<>(); } try (OperationContext ctx = new OperationContext(session)) { AutomationServer server = Framework.getService(AutomationServer.class); RestBinding binding = server.getOperationBinding(chainOrOperationId); if (binding != null && binding.isAdministrator()) { Principal principal = ctx.getPrincipal(); if (!(principal instanceof NuxeoPrincipal && ((NuxeoPrincipal) principal).isAdministrator())) { String message = "Not allowed. You must be administrator to use this operation"; log.error(message); throw new WebSecurityException(message); } } ctx.setInput(blobInput); ctx.putAll(contextParams); Object result = null; AutomationService as = Framework.getLocalService(AutomationService.class); // Drag and Drop action category is accessible from the chain sub context as chain parameters result = as.run(ctx, chainOrOperationId, operationParams); return result; } catch (OperationException e) { log.error("Error while executing automation batch ", e); throw new NuxeoException(e); } } protected int getUploadWaitTimeout() { String t = Framework.getProperty("org.nuxeo.batch.upload.wait.timeout", "5"); try { return Integer.parseInt(t); } catch (NumberFormatException e) { log.error("Wrong number format for upload wait timeout property", e); return 5; } } @Override public Object executeAndClean(String batchId, String chainOrOperationId, CoreSession session, Map<String, Object> contextParams, Map<String, Object> operationParams) { try { return execute(batchId, chainOrOperationId, session, contextParams, operationParams); } finally { clean(batchId); } } @Override public boolean removeFileEntry(String batchId, String filedIdx) { Batch batch = getBatch(batchId); if (batch == null) { return false; } return batch.removeFileEntry(filedIdx, getTransientStore()); } }