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