001/*
002 *  Copyright 2013 Anyware Services
003 *
004 *  Licensed under the Apache License, Version 2.0 (the "License");
005 *  you may not use this file except in compliance with the License.
006 *  You may obtain a copy of the License at
007 *
008 *      http://www.apache.org/licenses/LICENSE-2.0
009 *
010 *  Unless required by applicable law or agreed to in writing, software
011 *  distributed under the License is distributed on an "AS IS" BASIS,
012 *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
013 *  See the License for the specific language governing permissions and
014 *  limitations under the License.
015 */
016package org.ametys.cms.repository;
017
018import java.io.BufferedInputStream;
019import java.io.File;
020import java.io.FileInputStream;
021import java.io.IOException;
022import java.io.InputStream;
023import java.io.Reader;
024import java.nio.charset.Charset;
025import java.util.ArrayList;
026import java.util.HashMap;
027import java.util.List;
028import java.util.Map;
029
030import org.apache.avalon.framework.parameters.Parameters;
031import org.apache.avalon.framework.service.ServiceException;
032import org.apache.avalon.framework.service.ServiceManager;
033import org.apache.cocoon.acting.ServiceableAction;
034import org.apache.cocoon.environment.ObjectModelHelper;
035import org.apache.cocoon.environment.Redirector;
036import org.apache.cocoon.environment.Request;
037import org.apache.cocoon.environment.SourceResolver;
038import org.apache.cocoon.servlet.multipart.Part;
039import org.apache.cocoon.servlet.multipart.PartOnDisk;
040import org.apache.cocoon.servlet.multipart.RejectedPart;
041import org.apache.commons.io.FilenameUtils;
042import org.apache.commons.io.input.BOMInputStream;
043import org.apache.commons.lang.StringUtils;
044import org.apache.commons.lang3.ArrayUtils;
045import org.apache.tika.parser.txt.CharsetDetector;
046import org.supercsv.io.CsvListReader;
047import org.supercsv.io.ICsvListReader;
048import org.supercsv.prefs.CsvPreference;
049
050import org.ametys.cms.contenttype.ContentType;
051import org.ametys.cms.contenttype.ContentTypeExtensionPoint;
052import org.ametys.cms.workflow.ContentWorkflowHelper;
053import org.ametys.cms.workflow.CreateContentFunction;
054import org.ametys.cms.workflow.EditContentFunction;
055import org.ametys.core.cocoon.JSonReader;
056import org.ametys.plugins.repository.AmetysObjectResolver;
057import org.ametys.plugins.repository.AmetysRepositoryException;
058import org.ametys.plugins.workflow.AbstractWorkflowComponent;
059import org.ametys.plugins.workflow.support.WorkflowProvider;
060import org.ametys.plugins.workflow.support.WorkflowProvider.AmetysObjectWorkflow;
061
062import com.opensymphony.workflow.WorkflowException;
063
064/**
065 * Imports simple contents from a CSV file
066 *
067 */
068public class ImportSimpleContentsAction extends ServiceableAction
069{
070    private static final String[] _ALLOWED_EXTENSIONS = new String[] {"txt", "csv"};
071    
072    private ContentTypeExtensionPoint _contentTypeEP;
073
074    private WorkflowProvider _workflowProvider;
075    private AmetysObjectResolver _resolver;
076    //private org.apache.excalibur.source.SourceResolver _srcResolver;
077    private ContentWorkflowHelper _contentWorkflowHelper;
078    
079    @Override
080    public void service(ServiceManager smanager) throws ServiceException
081    {
082        super.service(smanager);
083        _contentTypeEP = (ContentTypeExtensionPoint) smanager.lookup(ContentTypeExtensionPoint.ROLE);
084        _workflowProvider = (WorkflowProvider) smanager.lookup(WorkflowProvider.ROLE);
085        _resolver = (AmetysObjectResolver) smanager.lookup(AmetysObjectResolver.ROLE);
086        //_srcResolver = (org.apache.excalibur.source.SourceResolver) smanager.lookup(org.apache.excalibur.source.SourceResolver.ROLE);
087        _contentWorkflowHelper = (ContentWorkflowHelper) smanager.lookup(ContentWorkflowHelper.ROLE);
088    }
089    
090    @Override
091    public Map act(Redirector redirector, SourceResolver resolver, Map objectModel, String source, Parameters parameters) throws Exception
092    {
093        Map<String, Object> result = new HashMap<>();
094        
095        Request request = ObjectModelHelper.getRequest(objectModel);
096        String cTypeId = request.getParameter("contentType");
097        String lang = request.getParameter("contentLanguage");
098        
099        ContentType cType = _contentTypeEP.getExtension(cTypeId);
100        if (!cType.isSimple())
101        {
102            result.put("error", "invalid-content-type");
103            request.setAttribute(JSonReader.OBJECT_TO_READ, result);
104            return EMPTY_MAP;
105        }
106        
107        Part part = (Part) request.get("importFile");
108        if (part instanceof RejectedPart)
109        {
110            result.put("error", "rejected-file");
111            request.setAttribute(JSonReader.OBJECT_TO_READ, result);
112            return EMPTY_MAP;
113        }
114        
115        PartOnDisk uploadedFilePart = (PartOnDisk) part;
116        File uploadedFile = (uploadedFilePart != null) ? uploadedFilePart.getFile() : null;
117        String filename = (uploadedFilePart != null) ? uploadedFilePart.getFileName().toLowerCase() : null;
118        
119        if (!FilenameUtils.isExtension(filename, _ALLOWED_EXTENSIONS))
120        {
121            result.put("error", "invalid-extension");
122            request.setAttribute(JSonReader.OBJECT_TO_READ, result);
123            return EMPTY_MAP;
124        }
125        
126        String workflowName = request.getParameter("workflowName");
127        int initWorkflowActionId = Integer.valueOf(request.getParameter("initWorkflowActionId"));
128        int workflowEditActionId = Integer.valueOf(request.getParameter("editWorkflowActionId"));
129
130        try
131        {
132            List<String> contentIds = _createSimpleContents(cTypeId, lang, workflowName, initWorkflowActionId, workflowEditActionId, uploadedFile);
133            result.put("contentIds", contentIds);
134            result.put("success", "true");
135        }
136        catch (Exception e)
137        {
138            result.put("error", "true");
139            result.put("errorMessage", e.getMessage());
140            
141            getLogger().error("Unable to create simple contents.", e);
142        }
143        
144        request.setAttribute(JSonReader.OBJECT_TO_READ, result);
145        
146        return EMPTY_MAP;
147    }
148
149    private List<String> _createSimpleContents (String contentTypeId, String lang, String workflowName, int initActionId, int editActionId, File uploadedFile) throws IOException, WorkflowException
150    {
151        List<String> contentIds = new ArrayList<>();
152        
153        try (ICsvListReader csvReader = new CsvListReader(_getReader(new BOMInputStream(new FileInputStream(uploadedFile))), CsvPreference.EXCEL_NORTH_EUROPE_PREFERENCE))
154        {
155            String[] metadataNames = csvReader.getHeader(true);
156            int titleIndex = ArrayUtils.indexOf(metadataNames, "title");
157            titleIndex = titleIndex != -1 ? titleIndex : 0;
158            
159            List<String> parts = new ArrayList<>();
160            while ((parts = csvReader.read()) != null)
161            {
162                String contentId = _createSimpleContent(contentTypeId, lang, parts.get(titleIndex), parts.get(titleIndex), workflowName, initActionId);
163                Content content = _resolver.resolveById(contentId);
164                    
165                _editSimpleContent(content, metadataNames, parts, editActionId);
166                    
167                contentIds.add(contentId);
168            }
169            
170        }
171        // FIXME catch WorkflowException and remove all created contents?
172        
173        return contentIds;
174    }
175    
176    /**
177     * Get a reader on the data stream that detects the charset.
178     * @param in the data stream.
179     * @return the reader with the correct character set.
180     */
181    protected Reader _getReader(InputStream in)
182    {
183        // Use Tika/ICU to detect the file charset.
184        BufferedInputStream buffIs = new BufferedInputStream(in);
185        
186        CharsetDetector detector = new CharsetDetector();
187        return detector.getReader(buffIs, Charset.defaultCharset().name());
188    }
189    
190    private Map<String, Object> _editSimpleContent (Content content, String[] metadataNames, List<String> values, int actionId) throws AmetysRepositoryException, WorkflowException
191    {
192        Map<String, String> jsValues = new HashMap<>();
193        
194        for (int i = 0; i < metadataNames.length; i++)
195        {
196            if (values.size() > i)
197            {
198                jsValues.put(EditContentFunction.FORM_ELEMENTS_PREFIX + metadataNames[i], StringUtils.trim(values.get(i)));
199            }
200        }
201
202        Map<String, Object> contextParameters = new HashMap<>();
203        contextParameters.put("quit", true);
204        contextParameters.put("values", jsValues);
205        contextParameters.put(EditContentFunction.METADATA_SET_PARAM, null);
206        
207        Map<String, Object> inputs = new HashMap<>();
208        inputs.put(AbstractWorkflowComponent.CONTEXT_PARAMETERS_KEY, contextParameters);
209        
210        Map<String, Object> result = _contentWorkflowHelper.doAction((WorkflowAwareContent) content, actionId, inputs);
211        return result;
212    }
213    
214    private String _createSimpleContent(String cType, String lang, String contentName, String contentTitle, String workflowName, int actionId) throws WorkflowException
215    {
216        // Create content
217        AmetysObjectWorkflow workflow = _workflowProvider.getAmetysObjectWorkflow();
218        Map<String, Object> inputs = _getInputsForCreation(cType, lang, contentName, contentTitle);
219        
220        workflow.initialize(workflowName, actionId, inputs);
221        
222        @SuppressWarnings("unchecked")
223        Map<String, Object> workflowResult = (Map<String, Object>) inputs.get(AbstractWorkflowComponent.RESULT_MAP_KEY);
224        return (String) workflowResult.get("contentId");
225    }
226    
227    private Map<String, Object> _getInputsForCreation (String cType, String lang, String contentName, String contentTitle)
228    {
229        Map<String, Object> inputs = new HashMap<>();
230           
231        inputs.put(AbstractWorkflowComponent.RESULT_MAP_KEY, new HashMap<String, Object>());
232        inputs.put(CreateContentFunction.CONTENT_NAME_KEY, contentName);
233        inputs.put(CreateContentFunction.CONTENT_TITLE_KEY, contentTitle);
234        inputs.put(CreateContentFunction.CONTENT_TYPES_KEY, new String[] {cType});
235        inputs.put(CreateContentFunction.CONTENT_LANGUAGE_KEY, lang);
236        inputs.put(AbstractWorkflowComponent.FAIL_CONDITIONS_KEY, new ArrayList<String>());
237        
238        return inputs;
239    }
240}