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