001/*
002 * (C) Copyright 2014 Nuxeo SA (http://nuxeo.com/) and others.
003 *
004 * All rights reserved. This program and the accompanying materials
005 * are made available under the terms of the GNU Lesser General Public License
006 * (LGPL) version 2.1 which accompanies this distribution, and is available at
007 * http://www.gnu.org/licenses/lgpl-2.1.html
008 *
009 * This library is distributed in the hope that it will be useful,
010 * but WITHOUT ANY WARRANTY; without even the implied warranty of
011 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
012 * Lesser General Public License for more details.
013 *
014 * Contributors:
015 *     Thomas Roger
016 */
017
018package org.nuxeo.ecm.webapp.action;
019
020import static org.apache.commons.logging.LogFactory.getLog;
021import static org.jboss.seam.ScopeType.CONVERSATION;
022import static org.jboss.seam.annotations.Install.FRAMEWORK;
023import static org.nuxeo.ecm.webapp.helpers.EventNames.USER_ALL_DOCUMENT_TYPES_SELECTION_CHANGED;
024
025import java.io.IOException;
026import java.io.Serializable;
027import java.util.ArrayList;
028import java.util.Collection;
029import java.util.Collections;
030import java.util.HashMap;
031import java.util.List;
032import java.util.Map;
033import java.util.Random;
034
035import javax.faces.application.FacesMessage;
036import javax.faces.context.FacesContext;
037import javax.servlet.http.HttpServletRequest;
038
039import org.apache.commons.logging.Log;
040import org.jboss.seam.annotations.In;
041import org.jboss.seam.annotations.Install;
042import org.jboss.seam.annotations.Name;
043import org.jboss.seam.annotations.Observer;
044import org.jboss.seam.annotations.Scope;
045import org.jboss.seam.annotations.web.RequestParameter;
046import org.jboss.seam.core.Events;
047import org.jboss.seam.faces.FacesMessages;
048import org.jboss.seam.international.Messages;
049import org.jboss.seam.international.StatusMessage;
050import org.nuxeo.ecm.automation.AutomationService;
051import org.nuxeo.ecm.automation.OperationChain;
052import org.nuxeo.ecm.automation.OperationContext;
053import org.nuxeo.ecm.automation.OperationException;
054import org.nuxeo.ecm.automation.OperationParameters;
055import org.nuxeo.ecm.automation.core.util.BlobList;
056import org.nuxeo.ecm.automation.core.util.DataModelProperties;
057import org.nuxeo.ecm.automation.server.jaxrs.batch.BatchManager;
058import org.nuxeo.ecm.core.api.Blob;
059import org.nuxeo.ecm.core.api.CoreSession;
060import org.nuxeo.ecm.core.api.DataModel;
061import org.nuxeo.ecm.core.api.DocumentModel;
062import org.nuxeo.ecm.core.api.IdRef;
063import org.nuxeo.ecm.core.api.NuxeoException;
064import org.nuxeo.ecm.core.api.impl.SimpleDocumentModel;
065import org.nuxeo.ecm.core.api.security.SecurityConstants;
066import org.nuxeo.ecm.platform.actions.Action;
067import org.nuxeo.ecm.platform.ui.web.api.NavigationContext;
068import org.nuxeo.ecm.platform.ui.web.api.WebActions;
069import org.nuxeo.ecm.platform.ui.web.rest.FancyNavigationHandler;
070import org.nuxeo.ecm.platform.ui.web.util.files.FileUtils;
071import org.nuxeo.ecm.platform.web.common.exceptionhandling.ExceptionHelper;
072import org.nuxeo.ecm.webapp.dnd.DndConfigurationHelper;
073import org.nuxeo.ecm.webapp.filemanager.FileManageActionsBean;
074import org.nuxeo.ecm.webapp.filemanager.NxUploadedFile;
075import org.nuxeo.runtime.api.Framework;
076import org.richfaces.event.FileUploadEvent;
077
078/**
079 * @since 6.0
080 */
081@Name("importActions")
082@Scope(CONVERSATION)
083@Install(precedence = FRAMEWORK)
084public class ImportActions implements Serializable {
085
086    private static final long serialVersionUID = 1L;
087
088    private static final Log log = getLog(ImportActions.class);
089
090    public static final String LABEL_IMPORT_PROBLEM = "label.bulk.import.documents.error";
091
092    public static final String LABEL_IMPORT_CANNOT_CREATE_ERROR = "label.bulk.import.documents.cannotCreateError";
093
094    public static final String DOCUMENTS_IMPORTED = "documentImported";
095
096    protected static Random random = new Random();
097
098    @In(create = true, required = false)
099    protected transient CoreSession documentManager;
100
101    @In(create = true)
102    protected NavigationContext navigationContext;
103
104    @In(create = true)
105    protected transient WebActions webActions;
106
107    @In(create = true)
108    protected transient DndConfigurationHelper dndConfigHelper;
109
110    @In(create = true, required = false)
111    protected FacesMessages facesMessages;
112
113    @In(create = true)
114    protected Map<String, String> messages;
115
116    @RequestParameter
117    protected String fancyboxFormId;
118
119    protected DocumentModel importDocumentModel;
120
121    protected Action selectedImportOption;
122
123    protected List<Action> importOptions;
124
125    protected String selectedImportFolderId;
126
127    protected String currentBatchId;
128
129    protected Collection<NxUploadedFile> uploadedFiles = null;
130
131    public DocumentModel getImportDocumentModel() {
132        if (importDocumentModel == null) {
133            importDocumentModel = new SimpleDocumentModel();
134        }
135        return importDocumentModel;
136    }
137
138    public String getSelectedImportOptionId() {
139        if (selectedImportOption == null) {
140            selectedImportOption = importOptions != null && importOptions.size() > 0 ? importOptions.get(0) : null;
141        }
142        return selectedImportOption != null ? selectedImportOption.getId() : null;
143    }
144
145    public void setSelectedImportOptionId(String id) {
146        for (Action importOption : importOptions) {
147            if (importOption.getId().equals(id)) {
148                selectedImportOption = importOption;
149                break;
150            }
151        }
152    }
153
154    public Action getSelectedImportOption() {
155        if (selectedImportOption == null) {
156            selectedImportOption = importOptions != null && importOptions.size() > 0 ? importOptions.get(0) : null;
157        }
158        return selectedImportOption;
159    }
160
161    public List<Action> getImportOptions(String dropContext) {
162        if (importOptions == null) {
163            importOptions = new ArrayList<>();
164            importOptions.addAll(webActions.getActionsList(dropContext));
165        }
166        return importOptions;
167    }
168
169    public String getSelectedImportFolderId() {
170        if (selectedImportFolderId == null) {
171            DocumentModel currentDocument = navigationContext.getCurrentDocument();
172            if (currentDocument == null) {
173                return null;
174            }
175            if (currentDocument.isFolder() && !"/".equals(currentDocument.getPathAsString())
176                    && documentManager.hasPermission(currentDocument.getRef(), SecurityConstants.ADD_CHILDREN)) {
177                selectedImportFolderId = currentDocument.getId();
178            } else {
179                // try to find the first folderish parent
180                List<DocumentModel> parents = documentManager.getParentDocuments(currentDocument.getRef());
181                Collections.reverse(parents);
182
183                for (DocumentModel parent : parents) {
184                    if (parent.isFolder()
185                            && documentManager.hasPermission(parent.getRef(), SecurityConstants.ADD_CHILDREN)) {
186                        selectedImportFolderId = parent.getId();
187                        break;
188                    }
189                }
190            }
191        }
192        return selectedImportFolderId;
193    }
194
195    public void setSelectedImportFolderId(String selectedImportFolderId) {
196        this.selectedImportFolderId = selectedImportFolderId;
197    }
198
199    /*
200     * ----- Document bulk import -----
201     */
202
203    public String generateBatchId() {
204        if (currentBatchId == null) {
205            BatchManager batchManager = Framework.getService(BatchManager.class);
206            currentBatchId = batchManager.initBatch();
207        }
208        return currentBatchId;
209    }
210
211    public boolean hasUploadedFiles() {
212        if (currentBatchId != null) {
213            BatchManager batchManager = Framework.getLocalService(BatchManager.class);
214            return batchManager.hasBatch(currentBatchId);
215        }
216        return false;
217    }
218
219    public String importDocuments() {
220        Map<String, Serializable> importOptionProperties = selectedImportOption.getProperties();
221        String chainOrOperationId = null;
222        if (importOptionProperties.containsKey("chainId")) {
223            chainOrOperationId = (String) importOptionProperties.get("chainId");
224        } else if (importOptionProperties.containsKey("operationId")) {
225            chainOrOperationId = (String) importOptionProperties.get("operationId");
226        } else {
227            // fallback on action id
228            chainOrOperationId = selectedImportOption.getId();
229        }
230
231        List<DataModel> dms = new ArrayList<>();
232        for (String schema : importDocumentModel.getSchemas()) {
233            dms.add(importDocumentModel.getDataModel(schema));
234        }
235        DataModelProperties properties = new DataModelProperties(dms, true);
236
237        Map<String, Object> contextParams = new HashMap<>();
238        contextParams.put("docMetaData", properties);
239        contextParams.put("currentDocument", selectedImportFolderId);
240
241        boolean resetBatchState = true;
242        try {
243            List<DocumentModel> importedDocuments;
244            if (dndConfigHelper.useHtml5DragAndDrop()) {
245                importedDocuments = importDocumentsThroughBatchManager(chainOrOperationId, contextParams);
246
247            } else {
248                importedDocuments = importDocumentsThroughUploadItems(chainOrOperationId, contextParams);
249            }
250
251            Events.instance().raiseEvent(DOCUMENTS_IMPORTED, importedDocuments, importDocumentModel);
252
253            if (selectedImportFolderId != null) {
254                return navigationContext.navigateToRef(new IdRef(selectedImportFolderId));
255            }
256        } catch (NuxeoException e) {
257            log.debug(e, e);
258            Throwable t = ExceptionHelper.unwrapException(e);
259            // FIXME NXP-XXXXX
260            if (t.getMessage().contains("Cannot create")) {
261                // do not reset state
262                resetBatchState = false;
263                FacesMessage message = new FacesMessage(FacesMessage.SEVERITY_ERROR, Messages.instance().get(
264                        LABEL_IMPORT_CANNOT_CREATE_ERROR), null);
265                FacesContext faces = FacesContext.getCurrentInstance();
266                if (fancyboxFormId != null && fancyboxFormId.startsWith(":")) {
267                    faces.addMessage(fancyboxFormId.substring(1), message);
268                } else {
269                    faces.addMessage(fancyboxFormId, message);
270                }
271                HttpServletRequest httpRequest = (HttpServletRequest) faces.getExternalContext().getRequest();
272                // avoid redirect for message to be displayed (and fancybox to
273                // be reopened)
274                httpRequest.setAttribute(FancyNavigationHandler.DISABLE_REDIRECT_FOR_URL_REWRITE, Boolean.TRUE);
275            } else {
276                facesMessages.addFromResourceBundle(StatusMessage.Severity.ERROR,
277                        Messages.instance().get(LABEL_IMPORT_PROBLEM));
278            }
279        } finally {
280            if (resetBatchState) {
281                // reset batch state
282                cancel();
283            }
284        }
285
286        return null;
287    }
288
289    @SuppressWarnings("unchecked")
290    protected List<DocumentModel> importDocumentsThroughBatchManager(String chainOrOperationId,
291            Map<String, Object> contextParams) {
292        BatchManager bm = Framework.getLocalService(BatchManager.class);
293        return (List<DocumentModel>) bm.execute(currentBatchId, chainOrOperationId, documentManager, contextParams,
294                null);
295    }
296
297    @SuppressWarnings("unchecked")
298    protected List<DocumentModel> importDocumentsThroughUploadItems(String chainOrOperationId,
299            Map<String, Object> contextParams) {
300        if (uploadedFiles == null) {
301            return Collections.emptyList();
302        }
303
304        try {
305            List<Blob> blobs = new ArrayList<>();
306            for (NxUploadedFile uploadItem : uploadedFiles) {
307                Blob blob = uploadItem.getBlob();
308                FileUtils.configureFileBlob(blob);
309                blobs.add(blob);
310            }
311
312            OperationContext ctx = new OperationContext(documentManager);
313            ctx.setInput(new BlobList(blobs));
314            ctx.putAll(contextParams);
315
316            AutomationService as = Framework.getLocalService(AutomationService.class);
317            if (chainOrOperationId.startsWith("Chain.")) {
318                return (List<DocumentModel>) as.run(ctx, chainOrOperationId.substring(6));
319            } else {
320                OperationChain chain = new OperationChain("operation");
321                OperationParameters params = new OperationParameters(chainOrOperationId, new HashMap<String, Object>());
322                chain.add(params);
323                return (List<DocumentModel>) as.run(ctx, chain);
324            }
325        } catch (OperationException e) {
326            log.error("Error while executing automation batch ", e);
327            throw new NuxeoException(e);
328        } finally {
329            for (NxUploadedFile uploadItem : getUploadedFiles()) {
330                // FIXME: check if a temp file needs to be tracked for
331                // deletion
332                // File tempFile = uploadItem.getFile();
333                // if (tempFile != null && tempFile.exists()) {
334                // Framework.trackFile(tempFile, tempFile);
335                // }
336            }
337            uploadedFiles = null;
338        }
339    }
340
341    public void cancel() {
342        if (currentBatchId != null) {
343            BatchManager bm = Framework.getLocalService(BatchManager.class);
344            bm.clean(currentBatchId);
345        }
346        importDocumentModel = null;
347        selectedImportFolderId = null;
348        uploadedFiles = null;
349        currentBatchId = null;
350    }
351
352    public Collection<NxUploadedFile> getUploadedFiles() {
353        if (uploadedFiles == null) {
354            uploadedFiles = new ArrayList<>();
355        }
356        return uploadedFiles;
357    }
358
359    public void setUploadedFiles(Collection<NxUploadedFile> uploadedFiles) {
360        this.uploadedFiles = uploadedFiles;
361    }
362
363    @Observer(value = USER_ALL_DOCUMENT_TYPES_SELECTION_CHANGED)
364    public void invalidateSelectedImportFolder() {
365        selectedImportFolderId = null;
366    }
367
368    /**
369     * @since 6.0
370     */
371    public void processUpload(FileUploadEvent uploadEvent) {
372        try {
373            if (uploadedFiles == null) {
374                uploadedFiles = new ArrayList<NxUploadedFile>();
375            }
376            Blob blob = FileManageActionsBean.getBlob(uploadEvent);
377            uploadedFiles.add(new NxUploadedFile(blob));
378        } catch (IOException e) {
379            log.error(e, e);
380        }
381    }
382
383}