001/* 002 * Copyright 2011 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.cms.workflow; 017 018import java.time.ZonedDateTime; 019import java.util.HashMap; 020import java.util.List; 021import java.util.Locale; 022import java.util.Map; 023import java.util.Map.Entry; 024import java.util.Optional; 025import java.util.concurrent.Future; 026 027import javax.jcr.Node; 028import javax.jcr.RepositoryException; 029import javax.jcr.Session; 030 031import org.apache.avalon.framework.service.ServiceException; 032import org.apache.avalon.framework.service.ServiceManager; 033import org.apache.commons.lang3.StringUtils; 034 035import org.ametys.cms.ObservationConstants; 036import org.ametys.cms.contenttype.ContentType; 037import org.ametys.cms.contenttype.ContentTypeExtensionPoint; 038import org.ametys.cms.contenttype.ContentTypesHelper; 039import org.ametys.cms.data.type.ModelItemTypeConstants; 040import org.ametys.cms.repository.Content; 041import org.ametys.cms.repository.ModifiableContent; 042import org.ametys.cms.repository.ModifiableWorkflowAwareContent; 043import org.ametys.core.observation.Event; 044import org.ametys.core.observation.ObservationManager; 045import org.ametys.core.user.UserIdentity; 046import org.ametys.core.user.population.UserPopulationDAO; 047import org.ametys.plugins.repository.AmetysObjectResolver; 048import org.ametys.plugins.repository.AmetysRepositoryException; 049import org.ametys.plugins.repository.ModifiableTraversableAmetysObject; 050import org.ametys.plugins.repository.RepositoryConstants; 051import org.ametys.plugins.repository.jcr.NameHelper; 052import org.ametys.plugins.repository.jcr.NameHelper.NameComputationMode; 053import org.ametys.plugins.repository.metadata.ModifiableCompositeMetadata; 054import org.ametys.plugins.workflow.store.AbstractJackrabbitWorkflowStore; 055import org.ametys.plugins.workflow.store.AmetysObjectWorkflowStore; 056import org.ametys.runtime.model.ModelItem; 057 058import com.opensymphony.module.propertyset.PropertySet; 059import com.opensymphony.workflow.FunctionProvider; 060import com.opensymphony.workflow.WorkflowException; 061import com.opensymphony.workflow.spi.WorkflowEntry; 062import com.opensymphony.workflow.spi.WorkflowStore; 063 064/** 065 * OSWorkflow function for creating a content. 066 */ 067public class CreateContentFunction extends AbstractContentWorkflowComponent implements FunctionProvider 068{ 069 /** Constant for storing the content name to use into the transient variables map. */ 070 public static final String CONTENT_NAME_KEY = CreateContentFunction.class.getName() + "$contentName"; 071 /** Constant for storing the content title to use into the transient variables map. */ 072 public static final String CONTENT_TITLE_KEY = CreateContentFunction.class.getName() + "$contentTitle"; 073 /** Constant for storing the content title variants (for multilingual content only) to use into the transient variables map. */ 074 public static final String CONTENT_TITLE_VARIANTS_KEY = CreateContentFunction.class.getName() + "$contentTitleVariants"; 075 /** Constant for storing the content types to use into the transient variables map. */ 076 public static final String CONTENT_TYPES_KEY = CreateContentFunction.class.getName() + "$contentTypes"; 077 /** Constant for storing the content mixins to use into the transient variables map. */ 078 public static final String CONTENT_MIXINS_KEY = CreateContentFunction.class.getName() + "$mixins"; 079 /** Constant for storing the content language to use into the transient variables map. */ 080 public static final String CONTENT_LANGUAGE_KEY = CreateContentFunction.class.getName() + "$contentLanguage"; 081 /** Constant for storing the parent content to use into the transient variables map. */ 082 public static final String PARENT_CONTENT_ID_KEY = CreateContentFunction.class.getName() + "$parentContentId"; 083 /** Constant for storing the parent content metadata path to use into the transient variables map. */ 084 public static final String PARENT_CONTENT_METADATA_PATH_KEY = CreateContentFunction.class.getName() + "$parentContentMetadataPath"; 085 /** Constant for storing the function allowing to get initial values into the transient variables map. */ 086 public static final String INITIAL_VALUE_SUPPLIER = CreateContentFunction.class.getName() + "$initialValueSupplier"; 087 /** Constant for storing the parent content id to use into the transient variables map. */ 088 public static final String PARENT_CONTEXT_VALUE = CreateContentFunction.class.getName() + "$parentContextValue"; 089 090 /** Ametys object resolver available to subclasses. */ 091 protected AmetysObjectResolver _resolver; 092 /** Observation manager available to subclasses. */ 093 protected ObservationManager _observationManager; 094 /** The content types handler */ 095 protected ContentTypeExtensionPoint _contentTypeEP; 096 /** The content types helper */ 097 protected ContentTypesHelper _contentTypeHelper; 098 099 @Override 100 public void service(ServiceManager manager) throws ServiceException 101 { 102 super.service(manager); 103 _resolver = (AmetysObjectResolver) manager.lookup(AmetysObjectResolver.ROLE); 104 _observationManager = (ObservationManager) manager.lookup(ObservationManager.ROLE); 105 _contentTypeEP = (ContentTypeExtensionPoint) manager.lookup(ContentTypeExtensionPoint.ROLE); 106 _contentTypeHelper = (ContentTypesHelper) manager.lookup(ContentTypesHelper.ROLE); 107 } 108 109 @Override 110 public void execute(Map transientVars, Map args, PropertySet ps) throws WorkflowException 111 { 112 _logger.info("Performing content creation"); 113 114 try 115 { 116 String desiredContentName = _getNonNullVar(transientVars, CONTENT_NAME_KEY, "Missing content name"); 117 118 String[] contentTypes = Optional.of(CONTENT_TYPES_KEY) 119 .map(transientVars::get) 120 .map(ctypes -> (String[]) ctypes) 121 .orElseThrow(() -> new WorkflowException("Missing contents types")); 122 123 String[] mixins = Optional.of(CONTENT_MIXINS_KEY) 124 .map(transientVars::get) 125 .map(m -> (String[]) m) 126 .orElse(new String[0]); 127 128 boolean multilingual = _isMultilingual(contentTypes); 129 130 String contentTitle = (String) transientVars.get(CONTENT_TITLE_KEY); 131 @SuppressWarnings("unchecked") 132 Map<String, String> contentTitleVariants = (Map<String, String>) transientVars.get(CONTENT_TITLE_VARIANTS_KEY); 133 134 if (contentTitle == null && contentTitleVariants == null) 135 { 136 throw new WorkflowException("Missing content title"); 137 } 138 139 String contentLanguage = (String) transientVars.get(CONTENT_LANGUAGE_KEY); 140 if (contentLanguage == null && !multilingual) 141 { 142 throw new WorkflowException("Missing content language for a non-multilingual content"); 143 } 144 145 ModifiableTraversableAmetysObject contents = _getContentRoot(transientVars); 146 147 ModifiableWorkflowAwareContent content = _createContent(transientVars, args, desiredContentName, contents); 148 content.setTypes(contentTypes); 149 content.setMixinTypes(mixins); 150 151 if (contentTitleVariants != null) 152 { 153 _setTitle(content, contentTypes, contentTitleVariants, contentLanguage != null ? new Locale(contentLanguage) : null); 154 } 155 else 156 { 157 content.setTitle(contentTitle, contentLanguage != null ? new Locale(contentLanguage) : null); 158 } 159 160 if (!multilingual) 161 { 162 content.setLanguage(contentLanguage); 163 } 164 165 // Set the workflow id 166 long workflowId = ((WorkflowEntry) transientVars.get("entry")).getId(); 167 content.setWorkflowId(workflowId); 168 169 _populateContent(transientVars, content); 170 171 // FIXME previous statements may have failed. 172 contents.saveChanges(); 173 174 _populateAdditionalData(transientVars, content); 175 176 Node node = content.getNode(); 177 Session session = node.getSession(); 178 179 _initWorkflow(transientVars, content, session, workflowId); 180 181 String parentContentId = (String) transientVars.get(PARENT_CONTENT_ID_KEY); 182 String parentContentMetadataPath = (String) transientVars.get(PARENT_CONTENT_METADATA_PATH_KEY); 183 if (StringUtils.isNotBlank(parentContentId) && StringUtils.isNotBlank(parentContentMetadataPath)) 184 { 185 node.setProperty("ametys-internal:subContent", true); 186 } 187 188 session.save(); 189 190 // Notify observers 191 _notifyContentAdded(content, transientVars); 192 193 // Content created 194 transientVars.put(CONTENT_KEY, content); 195 196 getResultsMap(transientVars).put("contentId", content.getId()); 197 getResultsMap(transientVars).put(CONTENT_KEY, content); 198 } 199 catch (RepositoryException e) 200 { 201 throw new WorkflowException("Unable to link the workflow to the content", e); 202 } 203 catch (AmetysRepositoryException e) 204 { 205 throw new WorkflowException("Unable to create the content", e); 206 } 207 } 208 209 /** 210 * Initialize the workflow of the added content. 211 * @param transientVars The workflow vars 212 * @param content The added content 213 * @param session The session 214 * @param workflowId The workflow ID 215 * @throws AmetysRepositoryException if a repository error occured 216 * @throws WorkflowException if a workflow error occured 217 */ 218 protected void _initWorkflow(Map transientVars, ModifiableWorkflowAwareContent content, Session session, long workflowId) throws AmetysRepositoryException, WorkflowException 219 { 220 try 221 { 222 WorkflowStore workflowStore = (WorkflowStore) transientVars.get("store"); 223 224 if (workflowStore instanceof AmetysObjectWorkflowStore) 225 { 226 AmetysObjectWorkflowStore ametysObjectWorkflowStore = (AmetysObjectWorkflowStore) workflowStore; 227 ametysObjectWorkflowStore.bindAmetysObject(content); 228 } 229 230 if (workflowStore instanceof AbstractJackrabbitWorkflowStore) 231 { 232 AbstractJackrabbitWorkflowStore jackrabbitWorkflowStore = (AbstractJackrabbitWorkflowStore) workflowStore; 233 Node workflowEntryNode = jackrabbitWorkflowStore.getEntryNode(session, workflowId); 234 235 Integer actionId = (Integer) transientVars.get("actionId"); 236 if (actionId != null) 237 { 238 workflowEntryNode.setProperty("ametys-internal:initialActionId", actionId); 239 } 240 } 241 } 242 catch (RepositoryException e) 243 { 244 throw new AmetysRepositoryException("Unable to link the workflow to the content", e); 245 } 246 } 247 248 /** 249 * Set the content's title variants 250 * @param content The content 251 * @param cTypes The content's content types 252 * @param titleVariants The title's variants 253 * @param locale The title's locale. Cannot be null if content is a non-multilingual content. 254 * @throws AmetysRepositoryException if failed to set title 255 */ 256 protected void _setTitle(ModifiableContent content, String[] cTypes, Map<String, String> titleVariants, Locale locale) throws AmetysRepositoryException 257 { 258 ModelItem modelItem = content.getDefinition(Content.ATTRIBUTE_TITLE); 259 if (ModelItemTypeConstants.MULTILINGUAL_STRING_ELEMENT_TYPE_ID.equals(modelItem.getType().getId())) 260 { 261 for (Entry<String, String> variant : titleVariants.entrySet()) 262 { 263 content.setTitle(variant.getValue(), new Locale(variant.getKey())); 264 } 265 } 266 else 267 { 268 if (locale == null) 269 { 270 throw new IllegalArgumentException("Cannot set a title from variants with null locale on a non-multilingual content"); 271 } 272 273 if (!titleVariants.containsKey(locale.getLanguage())) 274 { 275 throw new IllegalArgumentException("Title variants do not contains value for the requested locale " + locale + " for non multilingual content"); 276 } 277 278 content.setTitle(titleVariants.get(locale.getLanguage())); 279 } 280 } 281 282 /** 283 * Determines if the content to create is a multilingual content 284 * @param contentTypes The content types of content to create 285 * @return true if multilingual 286 */ 287 protected boolean _isMultilingual(String[] contentTypes) 288 { 289 for (String cTypeId : contentTypes) 290 { 291 ContentType cType = _contentTypeEP.getExtension(cTypeId); 292 if (cType == null) 293 { 294 throw new IllegalArgumentException("The content type '" + cTypeId + "' does not exists"); 295 } 296 if (cType.isMultilingual()) 297 { 298 return true; 299 } 300 } 301 302 return false; 303 } 304 305 /** 306 * Notify observers that the content has been created 307 * @param content The content added 308 * @param transientVars The workflow vars 309 * @return The {@link Future} objects of the asynchronous observers 310 * @throws WorkflowException If an error occurred 311 */ 312 protected List<Future> _notifyContentAdded(Content content, Map transientVars) throws WorkflowException 313 { 314 return _observationManager.notify(new Event(ObservationConstants.EVENT_CONTENT_ADDED, getUser(transientVars), _eventParamsForContentAdded(content))); 315 } 316 317 /** 318 * Gets the event parameters sent in method {@link #_notifyContentAdded(Content, Map)} 319 * @param content The content added 320 * @return the event parameters 321 */ 322 protected Map<String, Object> _eventParamsForContentAdded(Content content) 323 { 324 Map<String, Object> eventParams = new HashMap<>(); 325 eventParams.put(ObservationConstants.ARGS_CONTENT, content); 326 eventParams.put(ObservationConstants.ARGS_CONTENT_ID, content.getId()); 327 328 return eventParams; 329 } 330 331 /** 332 * Create the content object. 333 * @param transientVars the workflow parameters. 334 * @param args The workflow function arguments 335 * @param desiredContentName the desired content name. 336 * @param contentsNode the content root node in the repository. 337 * @return the created Content. 338 */ 339 protected ModifiableWorkflowAwareContent _createContent(Map transientVars, Map args, String desiredContentName, ModifiableTraversableAmetysObject contentsNode) 340 { 341 String contentName = NameHelper.getUniqueAmetysObjectName(contentsNode, desiredContentName, _getNameComputationMode(transientVars, args), false); 342 return contentsNode.createChild(contentName, _getObjectType(transientVars, args)); 343 } 344 345 /** 346 * Return the type of the object to be created. 347 * Ex: ametys:defaultContent. 348 * @param transientVars The workflow vars 349 * @param args The workflow function arguments 350 * @return The type of the object to be used during content creation. 351 */ 352 protected String _getObjectType(Map transientVars, Map args) 353 { 354 return RepositoryConstants.NAMESPACE_PREFIX + ":defaultContent"; 355 } 356 357 /** 358 * Return the mode to compute the name if not unique. 359 * Ex: ametys:defaultContent. 360 * @param transientVars The workflow vars 361 * @param args The workflow function arguments 362 * @return The unique name to be used during content creation. 363 */ 364 protected NameComputationMode _getNameComputationMode(Map transientVars, Map args) 365 { 366 String computationMode = (String) args.get("nameComputationMode"); 367 if (StringUtils.isNotBlank(computationMode)) 368 { 369 try 370 { 371 return NameComputationMode.valueOf(computationMode.toUpperCase()); 372 } 373 catch (IllegalArgumentException e) 374 { 375 // Ignore 376 } 377 } 378 379 return _getDefaultNameComputationMode(); 380 } 381 382 /** 383 * Define the default computation mode 384 * @return the default computation mode 385 */ 386 protected NameComputationMode _getDefaultNameComputationMode() 387 { 388 return NameComputationMode.INCREMENTAL; 389 } 390 391 /** 392 * Retrieve the content root. 393 * @param transientVars the workflow parameters. 394 * @return the content root node. 395 * @throws WorkflowException if an error occurs 396 */ 397 protected ModifiableTraversableAmetysObject _getContentRoot(Map transientVars) throws WorkflowException 398 { 399 String parentContentId = (String) transientVars.get(PARENT_CONTENT_ID_KEY); 400 String parentContentMetadataPath = (String) transientVars.get(PARENT_CONTENT_METADATA_PATH_KEY); 401 402 if (StringUtils.isNotBlank(parentContentId) && StringUtils.isNotBlank(parentContentMetadataPath)) 403 { 404 return _getSubContentRoot(parentContentId, parentContentMetadataPath); 405 } 406 407 return _resolver.resolveByPath("/" + RepositoryConstants.NAMESPACE_PREFIX + ":contents"); 408 } 409 410 /** 411 * Get the content root when creating a sub-content. 412 * @param parentContentId the parent content ID. 413 * @param parentContentMetadataPath the path of the metadata in which to create the sub-content. 414 * @return the content collection metadata. 415 * @throws WorkflowException if an error occurs. 416 */ 417 protected ModifiableTraversableAmetysObject _getSubContentRoot(String parentContentId, String parentContentMetadataPath) throws WorkflowException 418 { 419 Content parentContent = _resolver.resolveById(parentContentId); 420 421 if (parentContent instanceof ModifiableContent) 422 { 423 ModifiableCompositeMetadata meta = ((ModifiableContent) parentContent).getMetadataHolder(); 424 425 String metaPath = parentContentMetadataPath.replace('/', '.'); 426 427 String[] metadatas = StringUtils.split(metaPath, '.'); 428 for (int i = 0; i < metadatas.length - 1; i++) 429 { 430 meta = meta.getCompositeMetadata(metadatas[i], true); 431 } 432 433 String metaName = metadatas[metadatas.length - 1]; 434 435 return meta.getObjectCollection(metaName, true); 436 } 437 else 438 { 439 throw new WorkflowException("The content " + parentContentId + " is not modifiable."); 440 } 441 } 442 443 /** 444 * Get a workflow parameter, throwing an exception if it is null. 445 * @param transientVars the workflow parameters. 446 * @param name the variable name. 447 * @param exceptionName label of the exception to throw if the variable is null. 448 * @return the variable value. 449 * @throws WorkflowException If an error occurred 450 */ 451 protected String _getNonNullVar(Map transientVars, String name, String exceptionName) throws WorkflowException 452 { 453 String value = (String) transientVars.get(name); 454 if (value == null) 455 { 456 throw new WorkflowException(exceptionName); 457 } 458 return value; 459 } 460 461 /** 462 * Populate the content. 463 * @param transientVars the transient variables. 464 * @param content the content. 465 * @throws WorkflowException if an error occurs. 466 */ 467 protected void _populateContent(Map transientVars, ModifiableContent content) throws WorkflowException 468 { 469 UserIdentity user = getUser(transientVars); 470 if (user == null) 471 { 472 // FIXME Login can be null when creating the content in the background environment. 473 user = getSystemUser(); 474 } 475 476 // Add standard metadata 477 content.setCreator(user); 478 content.setCreationDate(ZonedDateTime.now()); 479 content.setLastContributor(user); 480 content.setLastModified(ZonedDateTime.now()); 481 } 482 483 /** 484 * Get the user to use when the content is created in a background environment. 485 * @return The anonymous user 486 */ 487 protected UserIdentity getSystemUser () 488 { 489 return UserPopulationDAO.SYSTEM_USER_IDENTITY; 490 } 491 492 /** 493 * Populate the content. 494 * @param transientVars the transient variables. 495 * @param content the content. 496 * @throws WorkflowException if an error occurs. 497 */ 498 protected void _populateAdditionalData(Map transientVars, ModifiableContent content) throws WorkflowException 499 { 500 // Nothing to do. 501 } 502}