001/* 002 * Copyright 2018 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.web.content; 017 018import java.io.IOException; 019import java.io.InputStream; 020import java.util.ArrayList; 021import java.util.HashMap; 022import java.util.LinkedHashMap; 023import java.util.List; 024import java.util.Locale; 025import java.util.Map; 026import java.util.Map.Entry; 027import java.util.Optional; 028import java.util.stream.Collectors; 029 030import org.apache.avalon.framework.component.Component; 031import org.apache.avalon.framework.context.Context; 032import org.apache.avalon.framework.context.ContextException; 033import org.apache.avalon.framework.context.Contextualizable; 034import org.apache.avalon.framework.service.ServiceException; 035import org.apache.avalon.framework.service.ServiceManager; 036import org.apache.avalon.framework.service.Serviceable; 037import org.apache.cocoon.components.ContextHelper; 038import org.apache.cocoon.environment.Request; 039import org.apache.cocoon.servlet.multipart.Part; 040import org.apache.cocoon.xml.AttributesImpl; 041import org.apache.cocoon.xml.XMLUtils; 042import org.apache.commons.collections.MapUtils; 043import org.apache.commons.lang3.ArrayUtils; 044import org.apache.commons.lang3.StringUtils; 045import org.xml.sax.ContentHandler; 046import org.xml.sax.SAXException; 047 048import org.ametys.cms.contenttype.ContentAttributeDefinition; 049import org.ametys.cms.contenttype.ContentType; 050import org.ametys.cms.contenttype.ContentTypeExtensionPoint; 051import org.ametys.cms.data.Binary; 052import org.ametys.cms.data.type.ModelItemTypeConstants; 053import org.ametys.cms.data.type.ResourceElementTypeHelper; 054import org.ametys.cms.repository.Content; 055import org.ametys.cms.search.query.AndQuery; 056import org.ametys.cms.search.query.ContentLanguageQuery; 057import org.ametys.cms.search.query.ContentTypeQuery; 058import org.ametys.cms.search.query.DocumentTypeQuery; 059import org.ametys.cms.search.query.Query; 060import org.ametys.cms.search.solr.SearcherFactory.Searcher; 061import org.ametys.cms.workflow.ContentWorkflowHelper; 062import org.ametys.cms.workflow.EditContentFunction; 063import org.ametys.core.upload.Upload; 064import org.ametys.core.upload.UploadManager; 065import org.ametys.core.user.CurrentUserProvider; 066import org.ametys.plugins.repository.AmetysObjectIterable; 067import org.ametys.plugins.repository.AmetysRepositoryException; 068import org.ametys.plugins.repository.RepositoryConstants; 069import org.ametys.plugins.repository.model.ViewHelper; 070import org.ametys.plugins.repository.provider.RequestAttributeWorkspaceSelector; 071import org.ametys.plugins.workflow.AbstractWorkflowComponent; 072import org.ametys.runtime.i18n.I18nizableText; 073import org.ametys.runtime.model.ElementDefinition; 074import org.ametys.runtime.model.ModelHelper; 075import org.ametys.runtime.model.ModelItem; 076import org.ametys.runtime.model.ModelViewItemGroup; 077import org.ametys.runtime.model.ViewItemContainer; 078import org.ametys.runtime.model.type.DataContext; 079import org.ametys.runtime.model.type.ElementType; 080import org.ametys.runtime.plugin.component.AbstractLogEnabled; 081import org.ametys.web.frontoffice.FrontOfficeSearcherFactory; 082 083import com.google.common.collect.ArrayListMultimap; 084import com.google.common.collect.Multimap; 085import com.opensymphony.workflow.WorkflowException; 086 087/** 088 * Helper for creating and editing a content from the submitted form 089 * 090 */ 091public class FOContentCreationHelper extends AbstractLogEnabled implements Serviceable, Component, Contextualizable 092{ 093 /** The component role. */ 094 public static final String ROLE = FOContentCreationHelper.class.getName(); 095 096 private UploadManager _uploadManager; 097 098 private CurrentUserProvider _currentUserProvider; 099 100 private ContentWorkflowHelper _contentWorkflowHelper; 101 102 private Context _context; 103 104 private ContentTypeExtensionPoint _cTypeExtPt; 105 106 private FrontOfficeSearcherFactory _searcherFactory; 107 108 public void service(ServiceManager smanager) throws ServiceException 109 { 110 _cTypeExtPt = (ContentTypeExtensionPoint) smanager.lookup(ContentTypeExtensionPoint.ROLE); 111 _uploadManager = (UploadManager) smanager.lookup(UploadManager.ROLE); 112 _currentUserProvider = (CurrentUserProvider) smanager.lookup(CurrentUserProvider.ROLE); 113 _contentWorkflowHelper = (ContentWorkflowHelper) smanager.lookup(ContentWorkflowHelper.ROLE); 114 _searcherFactory = (FrontOfficeSearcherFactory) smanager.lookup(FrontOfficeSearcherFactory.ROLE); 115 } 116 117 public void contextualize(Context context) throws ContextException 118 { 119 _context = context; 120 } 121 122 /** 123 * Get the request 124 * @return the request 125 */ 126 protected Request _getRequest() 127 { 128 return ContextHelper.getRequest(_context); 129 } 130 131 /** 132 * SAX contents values for metadata of type CONTENT 133 * @param contentHandler The content handler to sax into 134 * @param contentType The content type 135 * @param rootTagName The root tag name 136 * @param language the current language 137 * @throws SAXException if an error occurs when saxing 138 */ 139 public void saxContentValues(ContentHandler contentHandler, ContentType contentType, String rootTagName, String language) throws SAXException 140 { 141 List<ModelItem> contentAttributes = ModelHelper.findModelItemsByType(contentType, ModelItemTypeConstants.CONTENT_ELEMENT_TYPE_ID); 142 for (ModelItem contentAttribute : contentAttributes) 143 { 144 AttributesImpl attrs = new AttributesImpl(); 145 attrs.addCDATAAttribute("name", contentAttribute.getPath().replace("/", ".")); 146 XMLUtils.startElement(contentHandler, rootTagName, attrs); 147 _saxContentEnumeratorValue(contentHandler, (ContentAttributeDefinition) contentAttribute, language); 148 XMLUtils.endElement(contentHandler, rootTagName); 149 } 150 } 151 152 153 /** 154 * Sax enumeration value for enum or an attribute of type content 155 * @param contentHandler The content handler to sax into 156 * @param attribute The attribute of type content 157 * @param language The current language 158 * @throws SAXException If an error occurred while saxing 159 */ 160 private void _saxContentEnumeratorValue(ContentHandler contentHandler, ContentAttributeDefinition attribute, String language) throws SAXException 161 { 162 Map<String, String> values = getContentValues(attribute.getContentTypeId(), language); 163 164 XMLUtils.startElement(contentHandler, "enumeration"); 165 for (Entry<String, String> entry : values.entrySet()) 166 { 167 AttributesImpl attrItem = new AttributesImpl(); 168 attrItem.addCDATAAttribute("value", entry.getKey()); 169 XMLUtils.startElement(contentHandler, "item", attrItem); 170 XMLUtils.createElement(contentHandler, "label", entry.getValue()); 171 XMLUtils.endElement(contentHandler, "item"); 172 } 173 XMLUtils.endElement(contentHandler, "enumeration"); 174 } 175 176 /** 177 * Get values for contents enumeration 178 * @param cTypeId The id of content type 179 * @param language The current language 180 * @return The contents 181 */ 182 protected Map<String, String> getContentValues(String cTypeId, String language) 183 { 184 try 185 { 186 Query query = null; 187 boolean multilingual = false; 188 if (StringUtils.isNotEmpty(cTypeId)) 189 { 190 query = new ContentTypeQuery(cTypeId); 191 multilingual = _cTypeExtPt.getExtension(cTypeId).isMultilingual(); 192 } 193 194 if (!multilingual) 195 { 196 query = query != null ? new AndQuery(query, new ContentLanguageQuery(language)) : new ContentLanguageQuery(language); 197 } 198 199 Searcher searcher = _searcherFactory.create().withQuery(query) 200 .addFilterQuery(new DocumentTypeQuery("content")) 201 .withLimits(0, Integer.MAX_VALUE); 202 203 AmetysObjectIterable<Content> contents = searcher.search(); 204 205 return contents.stream() 206 .collect(Collectors.toMap(Content::getId, c -> c.getTitle(new Locale(language)))) 207 .entrySet() 208 .stream() 209 .collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue, (e1, e2) -> e1, LinkedHashMap::new)); 210 } 211 catch (Exception e) 212 { 213 getLogger().error("Failed to get content enumeration for content type " + cTypeId, e); 214 return MapUtils.EMPTY_MAP; 215 } 216 } 217 218 /** 219 * Get the values for this content type from request 220 * @param request the request 221 * @param contentType the edited content type 222 * @param viewName the view name 223 * @param errors The errors to fill 224 * @return The values 225 */ 226 public Map<String, Object> getAndValidateFormValues(Request request, ContentType contentType, String viewName, Multimap<String, I18nizableText> errors) 227 { 228 Map<String, Object> values = _getFormValues(request, contentType.getView(viewName), StringUtils.EMPTY, errors); 229 errors.putAll(validateValues(values, contentType, viewName)); 230 return values; 231 } 232 233 private Map<String, Object> _getFormValues(Request request, ViewItemContainer viewItemContainer, String prefix, Multimap<String, I18nizableText> errors) 234 { 235 Map<String, Object> values = new HashMap<>(); 236 237 ViewHelper.visitView(viewItemContainer, 238 (element, definition) -> { 239 // simple element 240 String name = definition.getName(); 241 String dataPath = prefix + name; 242 _getElementValue(request, definition, dataPath, errors) 243 .ifPresent(value -> values.put(name, value)); 244 }, 245 (group, definition) -> { 246 // composite 247 String name = definition.getName(); 248 String updatedPrefix = prefix + name + ModelItem.ITEM_PATH_SEPARATOR; 249 250 values.put(name, _getFormValues(request, group, updatedPrefix, errors)); 251 }, 252 (group, definition) -> { 253 // repeater 254 String name = definition.getName(); 255 String dataPath = prefix + name; 256 257 List<Map<String, Object>> entries = _getRepeaterEntries(request, group, dataPath, errors); 258 values.put(name, entries); 259 }, 260 group -> { 261 values.putAll(_getFormValues(request, group, prefix, errors)); 262 }); 263 264 return values; 265 } 266 267 @SuppressWarnings("unchecked") 268 private Optional<? extends Object> _getElementValue(Request request, ElementDefinition definition, String dataPath, Multimap<String, I18nizableText> errors) 269 { 270 Optional<? extends Object> value = Optional.empty(); 271 272 String fieldName = StringUtils.replace(dataPath, ModelItem.ITEM_PATH_SEPARATOR, "."); 273 Object valueFromRequest = request.get(fieldName); 274 275 if (valueFromRequest != null) 276 { 277 if (definition.isMultiple()) 278 { 279 List<Object> multipleValue; 280 if (valueFromRequest instanceof List) 281 { 282 multipleValue = (List<Object>) valueFromRequest; 283 } 284 else 285 { 286 multipleValue = List.of(valueFromRequest); 287 } 288 List<? extends Object> valuesAsList = multipleValue.stream() 289 .map(v -> _getTypedValue(v, definition, dataPath, errors)) 290 .filter(Optional::isPresent) 291 .map(Optional::get) 292 .collect(Collectors.toList()); 293 294 value = Optional.of(valuesAsList.toArray(new Object[valuesAsList.size()])); 295 } 296 else 297 { 298 Object singleValue; 299 if (valueFromRequest instanceof List) 300 { 301 List<Object> valuesFromRequest = (List<Object>) valueFromRequest; 302 singleValue = valuesFromRequest.isEmpty() ? null : valuesFromRequest.get(0); 303 } 304 else 305 { 306 singleValue = valueFromRequest; 307 } 308 value = _getTypedValue(singleValue, definition, dataPath, errors); 309 } 310 } 311 312 return value; 313 } 314 315 private Optional<? extends Object> _getTypedValue(Object formValue, ElementDefinition definition, String dataPath, Multimap<String, I18nizableText> errors) 316 { 317 Optional<? extends Object> value; 318 ElementType type = definition.getType(); 319 if (ModelItemTypeConstants.FILE_ELEMENT_TYPE_ID.equals(type.getId()) || ModelItemTypeConstants.BINARY_ELEMENT_TYPE_ID.equals(type.getId())) 320 { 321 value = _getUploadFileValue((Part) formValue, dataPath, errors); 322 } 323 else if (org.ametys.runtime.model.type.ModelItemTypeConstants.BOOLEAN_TYPE_ID.equals(type.getId())) 324 { 325 value = Optional.of("on".equals(formValue) || "true".equals(formValue)); 326 } 327 else 328 { 329 value = Optional.ofNullable(type.fromJSONForClient(formValue, DataContext.newInstance().withDataPath(dataPath))); 330 } 331 332 return value; 333 } 334 335 private List<Map<String, Object>> _getRepeaterEntries(Request request, ModelViewItemGroup viewItem, String dataPath, Multimap<String, I18nizableText> errors) 336 { 337 List<Map<String, Object>> entries = new ArrayList<>(); 338 339 String fieldName = StringUtils.replace(dataPath, ModelItem.ITEM_PATH_SEPARATOR, "."); 340 int repeaterSize = Optional.ofNullable(request.getParameter(fieldName + ".size")) 341 .map(Integer::valueOf) 342 .orElse(0); 343 344 for (int position = 1; position <= repeaterSize; position++) 345 { 346 String updatedPrefix = dataPath + "[" + position + "]" + ModelItem.ITEM_PATH_SEPARATOR; 347 entries.add(_getFormValues(request, viewItem, updatedPrefix, errors)); 348 } 349 350 return entries; 351 } 352 353 /** 354 * Validate the given values 355 * @param values the values to validate 356 * @param contentType the edited content type 357 * @param viewName the view name 358 * @return The errors if some values are not valid 359 */ 360 public Multimap<String, I18nizableText> validateValues(Map<String, Object> values, ContentType contentType, String viewName) 361 { 362 Multimap<String, I18nizableText> errors = ArrayListMultimap.create(); 363 364 Map<String, List<I18nizableText>> errorsAsMap = ViewHelper.validateValues(contentType.getView(viewName), Optional.ofNullable(values)); 365 for (String dataPath : errorsAsMap.keySet()) 366 { 367 errors.putAll(dataPath, errorsAsMap.get(dataPath)); 368 } 369 370 return errors; 371 } 372 373 /** 374 * Create and edit a content 375 * @param initActionId The initial workflow action id for creation and edition 376 * @param contentTypeId The id of content type 377 * @param siteName The current site name 378 * @param contentName The content name 379 * @param contentTitle The content title 380 * @param language The content language 381 * @param values The submitted values 382 * @param workflowName The workflow name 383 * @param viewName The view name 384 * @return The workflow result 385 * @throws AmetysRepositoryException if an error occurs 386 * @throws WorkflowException if an error occurs 387 */ 388 public Map<String, Object> createAndEditContent(int initActionId, String contentTypeId, String siteName, String contentName, String contentTitle, String language, Map<String, Object> values, String workflowName, String viewName) throws AmetysRepositoryException, WorkflowException 389 { 390 return createAndEditContent(initActionId, contentTypeId, siteName, contentName, contentTitle, language, values, workflowName, viewName, new HashMap<String, Object>()); 391 } 392 393 /** 394 * Create and edit a content 395 * @param initActionId The initial workflow action id for creation and edition 396 * @param contentTypeId The id of content type 397 * @param siteName The current site name 398 * @param contentName The content name 399 * @param contentTitle The content title 400 * @param language The content language 401 * @param values The submitted values 402 * @param workflowName The workflow name 403 * @param viewName The view name 404 * @param inputs The initial workflow inputs 405 * @return The workflow result 406 * @throws AmetysRepositoryException if an error occurs 407 * @throws WorkflowException if an error occurs 408 */ 409 public Map<String, Object> createAndEditContent(int initActionId, String contentTypeId, String siteName, String contentName, String contentTitle, String language, Map<String, Object> values, String workflowName, String viewName, Map<String, Object> inputs) throws AmetysRepositoryException, WorkflowException 410 { 411 return createAndEditContent(initActionId, new String[] {contentTypeId}, ArrayUtils.EMPTY_STRING_ARRAY, siteName, contentName, contentTitle, language, values, workflowName, viewName, inputs); 412 } 413 414 /** 415 * Create and edit a content 416 * @param initActionId The initial workflow action id for creation and edition 417 * @param contentTypeIds The new content types. Cannot be null. Cannot be empty. 418 * @param mixinIds The new mixins. Can be null. Can be empty. 419 * @param siteName The current site name 420 * @param contentName The content name 421 * @param contentTitle The content title 422 * @param language The content language 423 * @param values The values of the content attributes 424 * @param workflowName The workflow name 425 * @param viewName The view name 426 * @param inputs The initial workflow inputs 427 * @return The workflow result 428 * @throws AmetysRepositoryException if an error occurs 429 * @throws WorkflowException if an error occurs 430 */ 431 public Map<String, Object> createAndEditContent(int initActionId, String[] contentTypeIds, String[] mixinIds, String siteName, String contentName, String contentTitle, String language, Map<String, Object> values, String workflowName, String viewName, Map<String, Object> inputs) throws AmetysRepositoryException, WorkflowException 432 { 433 Request request = _getRequest(); 434 435 // Retrieve the current workspace. 436 String currentWsp = RequestAttributeWorkspaceSelector.getForcedWorkspace(request); 437 438 try 439 { 440 // Force the default workspace. 441 RequestAttributeWorkspaceSelector.setForcedWorkspace(request, RepositoryConstants.DEFAULT_WORKSPACE); 442 443 // Workflow parameters. 444 inputs.put(org.ametys.web.workflow.CreateContentFunction.SITE_KEY, siteName); 445 446 Map<String, Object> contextParameters = new HashMap<>(); 447 contextParameters.put(EditContentFunction.QUIT, true); 448 contextParameters.put(EditContentFunction.VALUES_KEY, values); 449 if (viewName != null) 450 { 451 contextParameters.put(EditContentFunction.VIEW_NAME, viewName); 452 } 453 454 inputs.put(AbstractWorkflowComponent.CONTEXT_PARAMETERS_KEY, contextParameters); 455 456 return _contentWorkflowHelper.createContent(workflowName, initActionId, contentName, contentTitle, contentTypeIds, mixinIds, language, null, null, inputs); 457 } 458 finally 459 { 460 // Restore context 461 RequestAttributeWorkspaceSelector.setForcedWorkspace(request, currentWsp); 462 } 463 464 } 465 466 private Optional<Binary> _getUploadFileValue(Part partUploaded, String dataPath, Multimap<String, I18nizableText> errors) 467 { 468 try (InputStream is = partUploaded.getInputStream()) 469 { 470 Upload upload = _uploadManager.storeUpload(_currentUserProvider.getUser(), partUploaded.getFileName(), is); 471 return Optional.of(ResourceElementTypeHelper.binaryFromUpload(upload)); 472 } 473 catch (IOException e) 474 { 475 getLogger().error("Unable to store uploaded file: " + partUploaded, e); 476 errors.put(dataPath, new I18nizableText("plugin.web", "PLUGINS_WEB_FO_HELPER_UPLOAD_FILE_ERROR")); 477 return Optional.empty(); 478 } 479 } 480 481 482}