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.function.Predicate; 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.components.source.impl.SitemapSource; 040import org.apache.cocoon.environment.Request; 041import org.apache.cocoon.servlet.multipart.Part; 042import org.apache.cocoon.xml.AttributesImpl; 043import org.apache.cocoon.xml.XMLUtils; 044import org.apache.commons.collections.MapUtils; 045import org.apache.commons.lang.StringUtils; 046import org.apache.commons.lang3.ArrayUtils; 047import org.apache.excalibur.source.SourceResolver; 048import org.xml.sax.ContentHandler; 049import org.xml.sax.SAXException; 050 051import org.ametys.cms.contenttype.ContentType; 052import org.ametys.cms.contenttype.ContentTypeExtensionPoint; 053import org.ametys.cms.contenttype.ContentTypesHelper; 054import org.ametys.cms.contenttype.MetadataDefinition; 055import org.ametys.cms.contenttype.MetadataType; 056import org.ametys.cms.repository.Content; 057import org.ametys.cms.search.query.AndQuery; 058import org.ametys.cms.search.query.ContentLanguageQuery; 059import org.ametys.cms.search.query.ContentTypeQuery; 060import org.ametys.cms.search.query.DocumentTypeQuery; 061import org.ametys.cms.search.query.Query; 062import org.ametys.cms.search.solr.SearcherFactory.Searcher; 063import org.ametys.cms.workflow.ContentWorkflowHelper; 064import org.ametys.cms.workflow.EditContentFunction; 065import org.ametys.core.upload.Upload; 066import org.ametys.core.upload.UploadManager; 067import org.ametys.core.user.CurrentUserProvider; 068import org.ametys.core.util.IgnoreRootHandler; 069import org.ametys.core.util.JSONUtils; 070import org.ametys.plugins.repository.AmetysObjectIterable; 071import org.ametys.plugins.repository.AmetysRepositoryException; 072import org.ametys.plugins.repository.RepositoryConstants; 073import org.ametys.plugins.repository.provider.RequestAttributeWorkspaceSelector; 074import org.ametys.plugins.workflow.AbstractWorkflowComponent; 075import org.ametys.runtime.i18n.I18nizableText; 076import org.ametys.runtime.parameter.Errors; 077import org.ametys.runtime.parameter.Validator; 078import org.ametys.runtime.plugin.component.AbstractLogEnabled; 079import org.ametys.web.frontoffice.FrontOfficeSearcherFactory; 080 081import com.google.common.collect.Multimap; 082import com.opensymphony.workflow.WorkflowException; 083 084/** 085 * Helper for creating and editing a content from the submitted form 086 * 087 */ 088public class FOContentCreationHelper extends AbstractLogEnabled implements Serviceable, Component, Contextualizable 089{ 090 /** The component role. */ 091 public static final String ROLE = FOContentCreationHelper.class.getName(); 092 093 private ContentTypesHelper _contentTypeHelper; 094 095 private JSONUtils _jsonUtils; 096 097 private UploadManager _uploadManager; 098 099 private CurrentUserProvider _currentUserProvider; 100 101 private ContentWorkflowHelper _contentWorkflowHelper; 102 103 private Context _context; 104 105 private SourceResolver _srcResolver; 106 107 private ContentTypeExtensionPoint _cTypeExtPt; 108 109 private FrontOfficeSearcherFactory _searcherFactory; 110 111 public void service(ServiceManager smanager) throws ServiceException 112 { 113 _contentTypeHelper = (ContentTypesHelper) smanager.lookup(ContentTypesHelper.ROLE); 114 _cTypeExtPt = (ContentTypeExtensionPoint) smanager.lookup(ContentTypeExtensionPoint.ROLE); 115 _jsonUtils = (JSONUtils) smanager.lookup(JSONUtils.ROLE); 116 _uploadManager = (UploadManager) smanager.lookup(UploadManager.ROLE); 117 _currentUserProvider = (CurrentUserProvider) smanager.lookup(CurrentUserProvider.ROLE); 118 _contentWorkflowHelper = (ContentWorkflowHelper) smanager.lookup(ContentWorkflowHelper.ROLE); 119 _srcResolver = (SourceResolver) smanager.lookup(SourceResolver.ROLE); 120 _searcherFactory = (FrontOfficeSearcherFactory) smanager.lookup(FrontOfficeSearcherFactory.ROLE); 121 } 122 123 public void contextualize(Context context) throws ContextException 124 { 125 _context = context; 126 } 127 128 /** 129 * Get the request 130 * @return the request 131 */ 132 protected Request _getRequest() 133 { 134 return ContextHelper.getRequest(_context); 135 } 136 137 /** 138 * SAX the view of a content type 139 * @param contentHandler The content handler to sax into 140 * @param contentType The content type 141 * @param rootTagName the root tag name 142 * @param viewName The view name 143 * @throws SAXException if an error occurs 144 */ 145 public void saxViewIfExists(ContentHandler contentHandler, ContentType contentType, String rootTagName, String viewName) throws SAXException 146 { 147 if (contentType != null && contentType.getView(viewName) != null) 148 { 149 AttributesImpl attrs = new AttributesImpl(); 150 attrs.addCDATAAttribute("id", contentType.getId()); 151 XMLUtils.startElement(contentHandler, rootTagName); 152 153 // FIXME Use new model API 154 String uri = "cocoon://_content-type/metadataset.xml?contentTypeId=" + contentType.getId() + "&viewName=" + viewName; 155 SitemapSource src = null; 156 157 try 158 { 159 src = (SitemapSource) _srcResolver.resolveURI(uri); 160 src.toSAX(new IgnoreRootHandler(contentHandler)); 161 } 162 catch (IOException e) 163 { 164 getLogger().error("Unable to sax view '" + viewName + "' for content type '" + contentType.getId() + "'", e); 165 } 166 finally 167 { 168 _srcResolver.release(src); 169 } 170 171 172 XMLUtils.endElement(contentHandler, rootTagName); 173 } 174 } 175 176 /** 177 * SAX contents values for metadata of type CONTENT 178 * @param contentHandler The content handler to sax into 179 * @param contentType The content type 180 * @param rootTagName The root tag name 181 * @param language the current language 182 * @throws SAXException if an error occurs when saxing 183 */ 184 public void saxContentValues(ContentHandler contentHandler, ContentType contentType, String rootTagName, String language) throws SAXException 185 { 186 Predicate<MetadataDefinition> p = md -> md.getType() == MetadataType.CONTENT; 187 Map<String, MetadataDefinition> contentDefs = _contentTypeHelper.getMetadataDefinitions(contentType, p); 188 189 for (MetadataDefinition metaDef : contentDefs.values()) 190 { 191 AttributesImpl attrs = new AttributesImpl(); 192 attrs.addCDATAAttribute("name", metaDef.getId().replace("/", ".")); 193 XMLUtils.startElement(contentHandler, rootTagName, attrs); 194 _saxContentEnumeratorValue(contentHandler, metaDef, language); 195 XMLUtils.endElement(contentHandler, rootTagName); 196 } 197 } 198 199 200 /** 201 * Sax enumeration value for enum or a content metadata 202 * @param contentHandler The content handler to sax into 203 * @param metadataDef The metadata definition. 204 * @param language The current language 205 * @throws SAXException If an error occurred while saxing 206 */ 207 private void _saxContentEnumeratorValue(ContentHandler contentHandler, MetadataDefinition metadataDef, String language) throws SAXException 208 { 209 Map<String, String> values = getContentValues(metadataDef.getContentType(), language); 210 211 XMLUtils.startElement(contentHandler, "enumeration"); 212 for (Entry<String, String> entry : values.entrySet()) 213 { 214 AttributesImpl attrItem = new AttributesImpl(); 215 attrItem.addCDATAAttribute("value", entry.getKey()); 216 XMLUtils.startElement(contentHandler, "item", attrItem); 217 XMLUtils.createElement(contentHandler, "label", entry.getValue()); 218 XMLUtils.endElement(contentHandler, "item"); 219 } 220 XMLUtils.endElement(contentHandler, "enumeration"); 221 } 222 223 /** 224 * Get values for contents enumeration 225 * @param cTypeId The id of content type 226 * @param language The current language 227 * @return The contents 228 */ 229 protected Map<String, String> getContentValues(String cTypeId, String language) 230 { 231 try 232 { 233 Query query = null; 234 boolean multilingual = false; 235 if (StringUtils.isNotEmpty(cTypeId)) 236 { 237 query = new ContentTypeQuery(cTypeId); 238 multilingual = _cTypeExtPt.getExtension(cTypeId).isMultilingual(); 239 } 240 241 if (!multilingual) 242 { 243 query = query != null ? new AndQuery(query, new ContentLanguageQuery(language)) : new ContentLanguageQuery(language); 244 } 245 246 Searcher searcher = _searcherFactory.create().withQuery(query) 247 .addFilterQuery(new DocumentTypeQuery("content")) 248 .withLimits(0, Integer.MAX_VALUE); 249 250 AmetysObjectIterable<Content> contents = searcher.search(); 251 252 return contents.stream() 253 .collect(Collectors.toMap(Content::getId, c -> c.getTitle(new Locale(language)))) 254 .entrySet() 255 .stream() 256 .collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue, (e1, e2) -> e1, LinkedHashMap::new)); 257 } 258 catch (Exception e) 259 { 260 getLogger().error("Failed to get content enumeration for content type " + cTypeId, e); 261 return MapUtils.EMPTY_MAP; 262 } 263 } 264 265 /** 266 * Get the values for this content type from request 267 * @param rawValues the input values 268 * @param contentType the edited content type 269 * @param viewName the view name 270 * @param errors The errors to fill 271 * @return The values 272 */ 273 public Map<String, Object> getAndValidateFormValues(Map<String, Object> rawValues, ContentType contentType, String viewName, Multimap<String, I18nizableText> errors) 274 { 275 Map<String, Object> formValues = new HashMap<>(); 276 277 Map<String, MetadataDefinition> metadataDefs = _contentTypeHelper.getMetadataDefinitions(contentType.getMetadataSetForView(viewName), contentType); 278 279 for (Entry<String, MetadataDefinition> entry : metadataDefs.entrySet()) 280 { 281 String metadataName = entry.getKey(); 282 String fieldName = StringUtils.replace(metadataName, "/", "."); 283 MetadataDefinition metadataDef = entry.getValue(); 284 285 if (metadataDef.getType() != MetadataType.COMPOSITE) 286 { 287 Object rawValue = rawValues.get(metadataName); 288 if (_validateFormField(metadataDef, fieldName, rawValue, errors)) 289 { 290 formValues.put(EditContentFunction.FORM_ELEMENTS_PREFIX + metadataName, rawValue); 291 } 292 } 293 } 294 295 return formValues; 296 } 297 298 /** 299 * Get the values for this content type from request 300 * @param request the request 301 * @param contentType the edited content type 302 * @param viewName the view name 303 * @param errors The errors to fill 304 * @return The values 305 */ 306 public Map<String, Object> getAndValidateFormValues(Request request, ContentType contentType, String viewName, Multimap<String, I18nizableText> errors) 307 { 308 Map<String, Object> rawValues = new HashMap<>(); 309 310 Map<String, MetadataDefinition> metadataDefs = _contentTypeHelper.getMetadataDefinitions(contentType.getMetadataSetForView(viewName), contentType); 311 312 for (Entry<String, MetadataDefinition> entry : metadataDefs.entrySet()) 313 { 314 String parameterName = entry.getKey(); 315 String fieldName = StringUtils.replace(parameterName, "/", "."); 316 MetadataDefinition metadataDef = entry.getValue(); 317 MetadataType type = metadataDef.getType(); 318 319 if (type == MetadataType.FILE || type == MetadataType.BINARY) 320 { 321 Part partUploaded = (Part) request.get(fieldName); 322 if (partUploaded != null) 323 { 324 Map<String, Object> uploadFile = _getUploadFileValue(partUploaded); 325 if (uploadFile != null) 326 { 327 rawValues.put(parameterName, uploadFile); 328 } 329 else 330 { 331 errors.put(fieldName, new I18nizableText("plugin.web", "PLUGINS_WEB_FO_HELPER_UPLOAD_FILE_ERROR")); 332 } 333 } 334 } 335 else if (type == MetadataType.BOOLEAN) 336 { 337 String value = request.getParameter(fieldName); 338 rawValues.put(parameterName, "on".equals(value) || "true".equals(value) ? true : false); 339 } 340 else if ((type == MetadataType.STRING || type == MetadataType.CONTENT) && metadataDef.isMultiple() && metadataDef.getEnumerator() == null) 341 { 342 // handle values such as name=value1&name=value2 or name=value1,value2 343 String[] values = request.getParameterValues(fieldName); 344 345 List<String> valuesAsList = new ArrayList<>(); 346 if (values != null && values.length > 0) 347 { 348 valuesAsList = Arrays.stream(values) 349 .filter(StringUtils::isNotBlank) 350 .map(v -> v.split(",")) 351 .flatMap(Arrays::stream) 352 .map(StringUtils::trim) 353 .collect(Collectors.toList()); 354 } 355 356 rawValues.put(parameterName, valuesAsList); 357 } 358 else if (metadataDef.getType() != MetadataType.COMPOSITE) 359 { 360 rawValues.put(parameterName, request.getParameter(fieldName)); 361 } 362 } 363 364 return getAndValidateFormValues(rawValues, contentType, viewName, errors); 365 } 366 367 /** 368 * Create and edit a content 369 * @param initActionId The initial workflow action id for creation and edition 370 * @param contentTypeId The id of content type 371 * @param siteName The current site name 372 * @param contentName The content name 373 * @param contentTitle The content title 374 * @param language The content language 375 * @param values The submitted values 376 * @param workflowName The workflow name 377 * @param viewName The view name 378 * @return The workflow result 379 * @throws AmetysRepositoryException if an error occurs 380 * @throws WorkflowException if an error occurs 381 */ 382 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 383 { 384 return createAndEditContent(initActionId, contentTypeId, siteName, contentName, contentTitle, language, values, workflowName, viewName, new HashMap<String, Object>()); 385 } 386 387 /** 388 * Create and edit a content 389 * @param initActionId The initial workflow action id for creation and edition 390 * @param contentTypeId The id of content type 391 * @param siteName The current site name 392 * @param contentName The content name 393 * @param contentTitle The content title 394 * @param language The content language 395 * @param values The submitted values 396 * @param workflowName The workflow name 397 * @param viewName The view name 398 * @param inputs The initial workflow inputs 399 * @return The workflow result 400 * @throws AmetysRepositoryException if an error occurs 401 * @throws WorkflowException if an error occurs 402 */ 403 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 404 { 405 return createAndEditContent(initActionId, new String[] {contentTypeId}, ArrayUtils.EMPTY_STRING_ARRAY, siteName, contentName, contentTitle, language, values, workflowName, viewName, inputs); 406 } 407 408 /** 409 * Create and edit a content 410 * @param initActionId The initial workflow action id for creation and edition 411 * @param contentTypeIds The new content types. Cannot be null. Cannot be empty. 412 * @param mixinIds The new mixins. Can be null. Can be empty. 413 * @param siteName The current site name 414 * @param contentName The content name 415 * @param contentTitle The content title 416 * @param language The content language 417 * @param values The submitted values 418 * @param workflowName The workflow name 419 * @param viewName The view name 420 * @param inputs The initial workflow inputs 421 * @return The workflow result 422 * @throws AmetysRepositoryException if an error occurs 423 * @throws WorkflowException if an error occurs 424 */ 425 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 426 { 427 Request request = _getRequest(); 428 429 // Retrieve the current workspace. 430 String currentWsp = RequestAttributeWorkspaceSelector.getForcedWorkspace(request); 431 432 try 433 { 434 // Force the default workspace. 435 RequestAttributeWorkspaceSelector.setForcedWorkspace(request, RepositoryConstants.DEFAULT_WORKSPACE); 436 437 // Workflow parameters. 438 inputs.put(org.ametys.web.workflow.CreateContentFunction.SITE_KEY, siteName); 439 440 Map<String, Object> contextParameters = new HashMap<>(); 441 contextParameters.put(EditContentFunction.QUIT, true); 442 contextParameters.put(EditContentFunction.FORM_RAW_VALUES, values); 443 if (viewName != null) 444 { 445 contextParameters.put(EditContentFunction.VIEW_NAME, viewName); 446 } 447 448 inputs.put(AbstractWorkflowComponent.CONTEXT_PARAMETERS_KEY, contextParameters); 449 450 return _contentWorkflowHelper.createContent(workflowName, initActionId, contentName, contentTitle, contentTypeIds, mixinIds, language, null, null, inputs); 451 } 452 finally 453 { 454 // Restore context 455 RequestAttributeWorkspaceSelector.setForcedWorkspace(request, currentWsp); 456 } 457 458 } 459 460 private boolean _validateFormField(MetadataDefinition metadataDef, String fieldName, Object value, Multimap<String, I18nizableText> errors) 461 { 462 Validator validator = metadataDef.getValidator(); 463 if (validator != null) 464 { 465 Errors fieldErrors = new Errors(); 466 validator.validate(value, fieldErrors); 467 if (fieldErrors.hasErrors()) 468 { 469 for (I18nizableText error : fieldErrors.getErrors()) 470 { 471 errors.put(fieldName, error); 472 } 473 474 return false; 475 } 476 } 477 478 return true; 479 } 480 481 private Map<String, Object> _getUploadFileValue(Part partUploaded) 482 { 483 Map<String, Object> file = new LinkedHashMap<>(); 484 485 Upload upload = null; 486 try (InputStream is = partUploaded.getInputStream()) 487 { 488 upload = _uploadManager.storeUpload(_currentUserProvider.getUser(), partUploaded.getFileName(), is); 489 490 file.put("id", upload.getId()); 491 file.put("filename", upload.getFilename()); 492 file.put("size", upload.getLength()); 493 file.put("viewHref", "/plugins/core/upload/file?id=" + upload.getId()); 494 file.put("downloadHref", "/plugins/core/upload/file?id=" + upload.getId() + "&download=true"); 495 file.put("type", "attribute"); 496 } 497 catch (IOException e) 498 { 499 getLogger().error("Unable to store uploaded file: " + partUploaded, e); 500 return null; 501 } 502 503 return file; 504 } 505}