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