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