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.util.Date; 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.FilterNameHelper; 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.RepositoryIntegrityViolationException; 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, 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 desiredContentName the desired content name. 335 * @param contentsNode the content root node in the repository. 336 * @return the created Content. 337 */ 338 protected ModifiableWorkflowAwareContent _createContent(Map transientVars, String desiredContentName, ModifiableTraversableAmetysObject contentsNode) 339 { 340 ModifiableWorkflowAwareContent content = null; 341 342 String contentName = FilterNameHelper.filterName(desiredContentName); 343 int errorCount = 0; 344 do 345 { 346 if (errorCount != 0) 347 { 348 contentName = FilterNameHelper.filterName(desiredContentName + " " + (errorCount + 1)); 349 } 350 try 351 { 352 String type = _getObjectType(transientVars); 353 content = contentsNode.createChild(contentName, type); 354 } 355 catch (RepositoryIntegrityViolationException e) 356 { 357 // Content name is already used 358 errorCount++; 359 } 360 } 361 while (content == null); 362 363 return content; 364 } 365 366 /** 367 * Return the type of the object to be created. 368 * Ex: ametys:defaultContent. 369 * @param transientVars The workflow vars 370 * @return The type of the object to be used during content creation. 371 */ 372 protected String _getObjectType(Map transientVars) 373 { 374 return RepositoryConstants.NAMESPACE_PREFIX + ":defaultContent"; 375 } 376 377 /** 378 * Retrieve the content root. 379 * @param transientVars the workflow parameters. 380 * @return the content root node. 381 * @throws WorkflowException if an error occurs 382 */ 383 protected ModifiableTraversableAmetysObject _getContentRoot(Map transientVars) throws WorkflowException 384 { 385 String parentContentId = (String) transientVars.get(PARENT_CONTENT_ID_KEY); 386 String parentContentMetadataPath = (String) transientVars.get(PARENT_CONTENT_METADATA_PATH_KEY); 387 388 if (StringUtils.isNotBlank(parentContentId) && StringUtils.isNotBlank(parentContentMetadataPath)) 389 { 390 return _getSubContentRoot(parentContentId, parentContentMetadataPath); 391 } 392 393 return _resolver.resolveByPath("/" + RepositoryConstants.NAMESPACE_PREFIX + ":contents"); 394 } 395 396 /** 397 * Get the content root when creating a sub-content. 398 * @param parentContentId the parent content ID. 399 * @param parentContentMetadataPath the path of the metadata in which to create the sub-content. 400 * @return the content collection metadata. 401 * @throws WorkflowException if an error occurs. 402 */ 403 protected ModifiableTraversableAmetysObject _getSubContentRoot(String parentContentId, String parentContentMetadataPath) throws WorkflowException 404 { 405 Content parentContent = _resolver.resolveById(parentContentId); 406 407 if (parentContent instanceof ModifiableContent) 408 { 409 ModifiableCompositeMetadata meta = ((ModifiableContent) parentContent).getMetadataHolder(); 410 411 String metaPath = parentContentMetadataPath.replace('/', '.'); 412 413 String[] metadatas = StringUtils.split(metaPath, '.'); 414 for (int i = 0; i < metadatas.length - 1; i++) 415 { 416 meta = meta.getCompositeMetadata(metadatas[i], true); 417 } 418 419 String metaName = metadatas[metadatas.length - 1]; 420 421 return meta.getObjectCollection(metaName, true); 422 } 423 else 424 { 425 throw new WorkflowException("The content " + parentContentId + " is not modifiable."); 426 } 427 } 428 429 /** 430 * Get a workflow parameter, throwing an exception if it is null. 431 * @param transientVars the workflow parameters. 432 * @param name the variable name. 433 * @param exceptionName label of the exception to throw if the variable is null. 434 * @return the variable value. 435 * @throws WorkflowException If an error occurred 436 */ 437 protected String _getNonNullVar(Map transientVars, String name, String exceptionName) throws WorkflowException 438 { 439 String value = (String) transientVars.get(name); 440 if (value == null) 441 { 442 throw new WorkflowException(exceptionName); 443 } 444 return value; 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 _populateContent(Map transientVars, ModifiableContent content) throws WorkflowException 454 { 455 UserIdentity user = getUser(transientVars); 456 if (user == null) 457 { 458 // FIXME Login can be null when creating the content in the background environment. 459 user = getSystemUser(); 460 } 461 462 // Add standard metadata 463 content.setCreator(user); 464 content.setCreationDate(new Date()); 465 content.setLastContributor(user); 466 content.setLastModified(new Date()); 467 } 468 469 /** 470 * Get the user to use when the content is created in a background environment. 471 * @return The anonymous user 472 */ 473 protected UserIdentity getSystemUser () 474 { 475 return UserPopulationDAO.SYSTEM_USER_IDENTITY; 476 } 477 478 /** 479 * Populate the content. 480 * @param transientVars the transient variables. 481 * @param content the content. 482 * @throws WorkflowException if an error occurs. 483 */ 484 protected void _populateAdditionalData(Map transientVars, ModifiableContent content) throws WorkflowException 485 { 486 // Nothing to do. 487 } 488}