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