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