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