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