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}