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}