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