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