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.contenttype.ContentTypesHelper;
053import org.ametys.cms.contenttype.MetadataDefinition;
054import org.ametys.cms.contenttype.MetadataType;
055import org.ametys.cms.workflow.AbstractContentWorkflowComponent;
056import org.ametys.cms.workflow.ContentWorkflowHelper;
057import org.ametys.cms.workflow.EditContentFunction;
058import org.ametys.core.cocoon.JSonReader;
059import org.ametys.core.util.JSONUtils;
060import org.ametys.plugins.repository.AmetysRepositoryException;
061import org.ametys.plugins.workflow.AbstractWorkflowComponent;
062
063import com.opensymphony.workflow.WorkflowException;
064
065/**
066 * Imports simple contents from a CSV file
067 *
068 */
069public class ImportSimpleContentsAction extends ServiceableAction
070{
071    private static final String[] _ALLOWED_EXTENSIONS = new String[] {"txt", "csv"};
072    
073    private ContentTypeExtensionPoint _contentTypeEP;
074    private ContentWorkflowHelper _contentWorkflowHelper;
075    private ContentTypeExtensionPoint _cTypeEP;
076
077    private ContentTypesHelper _contentTypeHelper;
078
079    private JSONUtils _jsonUtils;
080    
081    @Override
082    public void service(ServiceManager smanager) throws ServiceException
083    {
084        super.service(smanager);
085        _contentTypeEP = (ContentTypeExtensionPoint) smanager.lookup(ContentTypeExtensionPoint.ROLE);
086        _contentTypeHelper = (ContentTypesHelper) smanager.lookup(ContentTypesHelper.ROLE);
087        _contentWorkflowHelper = (ContentWorkflowHelper) smanager.lookup(ContentWorkflowHelper.ROLE);
088        _cTypeEP = (ContentTypeExtensionPoint) smanager.lookup(ContentTypeExtensionPoint.ROLE);
089        _jsonUtils = (JSONUtils) smanager.lookup(JSONUtils.ROLE);
090    }
091    
092    @Override
093    public Map act(Redirector redirector, SourceResolver resolver, Map objectModel, String source, Parameters parameters) throws Exception
094    {
095        Map<String, Object> result = new HashMap<>();
096        
097        Request request = ObjectModelHelper.getRequest(objectModel);
098        String cTypeId = request.getParameter("contentType");
099        String lang = request.getParameter("contentLanguage");
100        
101        ContentType cType = _contentTypeEP.getExtension(cTypeId);
102        if (!cType.isSimple())
103        {
104            result.put("error", "invalid-content-type");
105            request.setAttribute(JSonReader.OBJECT_TO_READ, result);
106            return EMPTY_MAP;
107        }
108        
109        Part part = (Part) request.get("importFile");
110        if (part instanceof RejectedPart)
111        {
112            result.put("error", "rejected-file");
113            request.setAttribute(JSonReader.OBJECT_TO_READ, result);
114            return EMPTY_MAP;
115        }
116        
117        PartOnDisk uploadedFilePart = (PartOnDisk) part;
118        File uploadedFile = (uploadedFilePart != null) ? uploadedFilePart.getFile() : null;
119        String filename = (uploadedFilePart != null) ? uploadedFilePart.getFileName().toLowerCase() : null;
120        
121        if (!FilenameUtils.isExtension(filename, _ALLOWED_EXTENSIONS))
122        {
123            result.put("error", "invalid-extension");
124            request.setAttribute(JSonReader.OBJECT_TO_READ, result);
125            return EMPTY_MAP;
126        }
127        
128        String workflowName = request.getParameter("workflowName");
129        int initWorkflowActionId = Integer.valueOf(request.getParameter("initWorkflowActionId"));
130        int workflowEditActionId = Integer.valueOf(request.getParameter("editWorkflowActionId"));
131
132        try
133        {
134            List<String> contentIds = _createSimpleContents(cTypeId, lang, workflowName, initWorkflowActionId, workflowEditActionId, uploadedFile);
135            result.put("contentIds", contentIds);
136            result.put("success", "true");
137        }
138        catch (Exception e)
139        {
140            result.put("error", "true");
141            result.put("errorMessage", e.getMessage());
142            
143            getLogger().error("Unable to create simple contents.", e);
144        }
145        
146        request.setAttribute(JSonReader.OBJECT_TO_READ, result);
147        
148        return EMPTY_MAP;
149    }
150
151    private List<String> _createSimpleContents (String contentTypeId, String lang, String workflowName, int initActionId, int editActionId, File uploadedFile) throws IOException, WorkflowException
152    {
153        List<String> contentIds = new ArrayList<>();
154        
155        try (ICsvListReader csvReader = new CsvListReader(_getReader(new BOMInputStream(new FileInputStream(uploadedFile))), CsvPreference.EXCEL_NORTH_EUROPE_PREFERENCE))
156        {
157            String[] metadataNames = csvReader.getHeader(true);
158            
159            // Remove spaces and tabs before and after the metadata name
160            for (int i = 0; i < metadataNames.length; i++)
161            {
162                metadataNames[i] = metadataNames[i].trim();
163            }
164            
165            int titleIndex = ArrayUtils.indexOf(metadataNames, "title");
166            titleIndex = titleIndex != -1 ? titleIndex : 0;
167            
168            List<String> parts = new ArrayList<>();
169            while ((parts = csvReader.read()) != null)
170            {
171                Content content = _createSimpleContent(contentTypeId, lang, parts.get(titleIndex).trim(), workflowName, initActionId);
172                _editSimpleContent(content, metadataNames, parts, editActionId, lang);
173                contentIds.add(content.getId());
174            }
175        }
176        // FIXME catch WorkflowException and remove all created contents?
177        return contentIds;
178    }
179    
180    /**
181     * Get a reader on the data stream that detects the charset.
182     * @param in the data stream.
183     * @return the reader with the correct character set.
184     */
185    protected Reader _getReader(InputStream in)
186    {
187        // Use Tika/ICU to detect the file charset.
188        BufferedInputStream buffIs = new BufferedInputStream(in);
189        
190        CharsetDetector detector = new CharsetDetector();
191        return detector.getReader(buffIs, Charset.defaultCharset().name());
192    }
193    
194    private Map<String, Object> _editSimpleContent (Content content, String[] metadataNames, List<String> values, int actionId, String defaultLang) throws AmetysRepositoryException, WorkflowException
195    {
196        Map<String, String> jsValues = new HashMap<>();
197        
198        for (int i = 0; i < metadataNames.length; i++)
199        {
200            if (!metadataNames[i].equals("title") && values.size() > i)
201            {
202                MetadataDefinition metadataDef = _contentTypeHelper.getMetadataDefinition(metadataNames[i], content);
203                if (metadataDef.getType() == MetadataType.MULTILINGUAL_STRING)
204                {
205                    Map<String, String> rawValues = new HashMap<>();
206                    rawValues.put(defaultLang, StringUtils.trim(values.get(i)));
207                    jsValues.put(EditContentFunction.FORM_ELEMENTS_PREFIX + metadataNames[i], _jsonUtils.convertObjectToJson(rawValues));
208                }
209                else
210                {
211                    jsValues.put(EditContentFunction.FORM_ELEMENTS_PREFIX + metadataNames[i], StringUtils.trim(values.get(i)));
212                }
213            }
214        }
215
216        Map<String, Object> contextParameters = new HashMap<>();
217        contextParameters.put("quit", true);
218        contextParameters.put("values", jsValues);
219        contextParameters.put(EditContentFunction.METADATA_SET_PARAM, null);
220        
221        Map<String, Object> inputs = new HashMap<>();
222        inputs.put(AbstractWorkflowComponent.CONTEXT_PARAMETERS_KEY, contextParameters);
223        
224        Map<String, Object> result = _contentWorkflowHelper.doAction((WorkflowAwareContent) content, actionId, inputs);
225        return result;
226    }
227    
228    private Content _createSimpleContent(String cTypeId, String lang, String contentTitle, String workflowName, int actionId) throws WorkflowException
229    {
230        ContentType cType = _cTypeEP.getExtension(cTypeId);
231        MetadataDefinition titleMetaDef = cType.getMetadataDefinition(DefaultContent.METADATA_TITLE);
232        
233        // CMS-9474 Import reference table : Title with only figures are not supported
234        String contentName = contentTitle;
235        if (!_startsWithAlpha(contentName))
236        {
237            contentName = "content-" + contentName;
238        }
239        
240        Map<String, Object> result = null;
241        if (titleMetaDef.getType() == MetadataType.MULTILINGUAL_STRING)
242        {
243            Map<String, String> titleVariants = new HashMap<>();
244            titleVariants.put(lang, contentTitle);
245            
246            result = _contentWorkflowHelper.createContent(workflowName, actionId, contentName, titleVariants, new String[] {cTypeId}, new String[0]);
247        }
248        else
249        {
250            result = _contentWorkflowHelper.createContent(workflowName, actionId, contentName, contentTitle, new String[] {cTypeId}, new String[0], lang);
251        }
252        
253        return (Content) result.get(AbstractContentWorkflowComponent.CONTENT_KEY);
254    }
255    
256    private boolean _startsWithAlpha(String stringToTest)
257    {
258        return stringToTest.toLowerCase().matches("^[a-zàèìòùáéíóúýâêîôûãñõäëïöüÿçßØøåæœ].*$");
259    }
260}