001/* 002 * Copyright 2020 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.plugins.contentio.csv; 017 018import java.io.BufferedReader; 019import java.io.IOException; 020import java.nio.charset.Charset; 021import java.nio.file.Files; 022import java.nio.file.Paths; 023import java.util.ArrayList; 024import java.util.Arrays; 025import java.util.HashMap; 026import java.util.List; 027import java.util.Map; 028 029import org.apache.avalon.framework.component.Component; 030import org.apache.avalon.framework.service.ServiceException; 031import org.apache.avalon.framework.service.ServiceManager; 032import org.apache.avalon.framework.service.Serviceable; 033import org.apache.commons.lang3.StringUtils; 034import org.supercsv.io.CsvListReader; 035import org.supercsv.io.ICsvListReader; 036import org.supercsv.prefs.CsvPreference; 037 038import org.ametys.cms.contenttype.ContentAttributeDefinition; 039import org.ametys.cms.contenttype.ContentType; 040import org.ametys.cms.contenttype.ContentTypeExtensionPoint; 041import org.ametys.core.ui.Callable; 042import org.ametys.runtime.model.ElementDefinition; 043import org.ametys.runtime.model.ModelViewItemGroup; 044import org.ametys.runtime.model.View; 045import org.ametys.runtime.model.ViewElement; 046import org.ametys.runtime.model.ViewElementAccessor; 047import org.ametys.runtime.model.ViewItem; 048import org.ametys.runtime.model.exception.BadItemTypeException; 049import org.ametys.runtime.plugin.component.AbstractLogEnabled; 050 051/** 052 * Import contents from an uploaded CSV file. 053 */ 054public class ImportCSVFileHelper extends AbstractLogEnabled implements Component, Serviceable 055{ 056 /** Avalon Role */ 057 public static final String ROLE = ImportCSVFileHelper.class.getName(); 058 059 private ContentTypeExtensionPoint _contentTypeEP; 060 061 private CSVImporter _csvImporter; 062 063 @Override 064 public void service(ServiceManager serviceManager) throws ServiceException 065 { 066 _contentTypeEP = (ContentTypeExtensionPoint) serviceManager.lookup(ContentTypeExtensionPoint.ROLE); 067 _csvImporter = (CSVImporter) serviceManager.lookup(CSVImporter.ROLE); 068 } 069 070 /** 071 * Gets the configuration for creating/editing a collection of synchronizable contents. 072 * @param contentTypeId Content type id 073 * @param formValues map of form values 074 * @param mappingValues list of header and content attribute mapping 075 * @return A map containing information about what is needed to create/edit a collection of synchronizable contents 076 * @throws IOException IOException while reading CSV 077 */ 078 @SuppressWarnings("unchecked") 079 @Callable 080 public Map<String, Object> validateConfiguration(String contentTypeId, Map formValues, List<Map<String, Object>> mappingValues) throws IOException 081 { 082 Map<String, Object> result = new HashMap<>(); 083 Map<String, Object> mapping = new HashMap<>(); 084 List<String> contentAttributes = new ArrayList<>(); 085 mappingValues.forEach(column -> 086 { 087 if (!StringUtils.isEmpty((String) column.get("attributePath"))) 088 { 089 generateNestedMap(mapping, (String) column.get("attributePath"), (String) column.get("header"), (boolean) column.get("isId")); 090 contentAttributes.add(((String) column.get("attributePath")).replace(".", "/")); 091 } 092 }); 093 ContentType contentType = _contentTypeEP.getExtension(contentTypeId); 094 View view = null; 095 try 096 { 097 view = View.of(Arrays.asList(contentType), contentAttributes.toArray(new String[0])); 098 } 099 catch (IllegalArgumentException | BadItemTypeException e) 100 { 101 getLogger().error("Error while creating view", e); 102 result.put("error", "bad-mapping"); 103 return result; 104 } 105 if (!mapping.containsKey("id")) 106 { 107 result.put("error", "missing-id"); 108 return result; 109 } 110 for (ViewItem viewItem : view.getViewItems()) 111 { 112 if (!_checkViewItemContainer(viewItem, (Map<String, Object>) mapping.get("values"), result)) 113 { 114 return result; 115 } 116 } 117 118 result.put("success", true); 119 return result; 120 } 121 /** 122 * Gets the configuration for creating/editing a collection of synchronizable contents. 123 * @param config get all CSV related parameters: path, separating/escaping char, charset and contentType 124 * @param formValues map of form values 125 * @param mappingValues list of header and content attribute mapping 126 * @return A map containing information about what is needed to create/edit a collection of synchronizable contents 127 * @throws IOException IOException while reading CSV 128 */ 129 @Callable 130 public Map<String, Object> importContents(Map config, Map formValues, List<Map<String, Object>> mappingValues) throws IOException 131 { 132 Map<String, Object> result = new HashMap<>(); 133 String path = (String) config.get("path"); 134 String separatingChar = (String) config.get("separating-char"); 135 String escapingChar = (String) config.get("escaping-char"); 136 String contentTypeId = (String) config.get("contentType"); 137 String encoding = (String) config.get("charset"); 138 CsvPreference csvPreference = new CsvPreference.Builder(escapingChar.charAt(0), separatingChar.charAt(0), "\r\n").build(); 139 Charset charset = Charset.forName(encoding); 140 String language = (String) formValues.get("language"); 141 int createAction = Integer.valueOf((String) formValues.get("createAction")); 142 int editAction = Integer.valueOf((String) formValues.get("editAction")); 143 String workflowName = (String) formValues.get("workflow"); 144 145 Map<String, Object> mapping = new HashMap<>(); 146 List<String> contentAttributes = new ArrayList<>(); 147 mappingValues.forEach(column -> 148 { 149 if (!StringUtils.isEmpty((String) column.get("attributePath"))) 150 { 151 generateNestedMap(mapping, (String) column.get("attributePath"), (String) column.get("header"), (boolean) column.get("isId")); 152 contentAttributes.add(((String) column.get("attributePath")).replace(".", "/")); 153 } 154 }); 155 156 ContentType contentType = _contentTypeEP.getExtension(contentTypeId); 157 View view = View.of(Arrays.asList(contentType), contentAttributes.toArray(new String[0])); 158 159 try (BufferedReader reader = Files.newBufferedReader(Paths.get(path), charset); 160 ICsvListReader csvMapReadernew = new CsvListReader(reader, csvPreference)) 161 { 162 Map<String, Object> importResult = _csvImporter.importContentsFromCSV(mapping, view, contentType, csvMapReadernew, createAction, editAction, workflowName, language); 163 164 @SuppressWarnings("unchecked") 165 List<String> contentIds = (List<String>) importResult.get("contentIds"); 166 if (contentIds.size() > 0) 167 { 168 result.put("importedCount", contentIds.size()); 169 result.put("nbErrors", importResult.get("nbErrors")); 170 result.put("nbWarnings", importResult.get("nbWarnings")); 171 } 172 else 173 { 174 result.put("error", "no-import"); 175 } 176 } 177 catch (Exception e) 178 { 179 getLogger().error("Error while importing contents", e); 180 result.put("error", "error"); 181 } 182 finally 183 { 184 deleteFile(path); 185 186 } 187 188 return result; 189 } 190 191 private boolean _checkViewItemContainer(ViewItem viewItem, Map<String, Object> mapping, Map<String, Object> result) 192 { 193 if (viewItem instanceof ViewElement) 194 { 195 ViewElement viewElement = (ViewElement) viewItem; 196 ElementDefinition elementDefinition = viewElement.getDefinition(); 197 if (elementDefinition instanceof ContentAttributeDefinition) 198 { 199 ViewElementAccessor viewElementAccessor = (ViewElementAccessor) viewElement; 200 ContentAttributeDefinition contentAttributeDefinition = (ContentAttributeDefinition) elementDefinition; 201 return _checkViewItemContentContainer(viewElementAccessor, mapping, result, contentAttributeDefinition); 202 } 203 else 204 { 205 return true; 206 } 207 } 208 else if (viewItem instanceof ModelViewItemGroup) 209 { 210 return _checkViewItemGroupContainer(viewItem, mapping, result); 211 } 212 else 213 { 214 result.put("error", "unknown-type"); 215 result.put("details", viewItem.getName()); 216 return false; 217 } 218 } 219 220 @SuppressWarnings("unchecked") 221 private boolean _checkViewItemContentContainer(ViewElementAccessor viewElementAccessor, Map<String, Object> mapping, Map<String, Object> result, ContentAttributeDefinition contentAttributeDefinition) 222 { 223 224 boolean success = true; 225 String contentTypeId = contentAttributeDefinition.getContentTypeId(); 226 ContentType contentType = _contentTypeEP.getExtension(contentTypeId); 227 228 Map<String, Object> nestedMap = (Map<String, Object>) mapping.get(viewElementAccessor.getName()); 229 if (!nestedMap.containsKey("id")) 230 { 231 result.put("error", "missing-id-content"); 232 result.put("details", List.of(contentType.getLabel(), contentAttributeDefinition.getName())); 233 return false; 234 } 235 if (contentType.isAbstract()) 236 { 237 result.put("error", "abstract-content"); 238 result.put("details", List.of(contentType.getLabel(), contentAttributeDefinition.getName())); 239 return false; 240 } 241 Map<String, Object> nestedMapValues = (Map<String, Object>) (nestedMap.get("values")); 242 for (ViewItem nestedViewItem : viewElementAccessor.getViewItems()) 243 { 244 success = success || _checkViewItemContainer(nestedViewItem, nestedMapValues, result); 245 } 246 return success; 247 } 248 249 @SuppressWarnings("unchecked") 250 private boolean _checkViewItemGroupContainer(ViewItem viewItem, Map<String, Object> mapping, Map<String, Object> result) 251 { 252 boolean success = true; 253 ModelViewItemGroup modelViewItemGroup = (ModelViewItemGroup) viewItem; 254 List<ViewItem> elementDefinition = modelViewItemGroup.getViewItems(); 255 Map<String, Object> nestedMap = (Map<String, Object>) mapping.get(viewItem.getName()); 256 257 Map<String, Object> nestedMapValues = (Map<String, Object>) (nestedMap.get("values")); 258 259 for (ViewItem nestedViewItem : elementDefinition) 260 { 261 success = success && _checkViewItemContainer(nestedViewItem, nestedMapValues, result); 262 } 263 return success; 264 } 265 266 @SuppressWarnings("unchecked") 267 private static Map<String, Object> generateNestedMap(Map<String, Object> map, String attributePath, String column, boolean isId) 268 { 269 int dotIndex = attributePath.indexOf('.'); 270 271 if (!map.containsKey("values")) 272 { 273 map.put("values", new HashMap<String, Object>()); 274 } 275 276 if (dotIndex == -1) 277 { 278 if (Boolean.valueOf(isId)) 279 { 280 map.put("id", attributePath); 281 } 282 ((Map<String, Object>) map.get("values")).put(attributePath, column); 283 } 284 else 285 { 286 String prefix = attributePath.split("\\.")[0]; 287 String subAttribute = attributePath.substring(dotIndex + 1); 288 289 if (!((Map<String, Object>) map.get("values")).containsKey(prefix)) 290 { 291 ((Map<String, Object>) map.get("values")).put(prefix, new HashMap<String, Object>()); 292 } 293 294 generateNestedMap((Map<String, Object>) ((Map<String, Object>) map.get("values")).get(prefix), subAttribute, column, isId); 295 } 296 return map; 297 } 298 299 /** 300 * Delete the file related to the given path 301 * @param path path of the file 302 * @throws IOException if an error occurs while deleting the file 303 */ 304 @Callable 305 public void deleteFile(String path) throws IOException 306 { 307 Files.delete(Paths.get(path)); 308 } 309}