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.Map; 022import java.util.concurrent.Future; 023 024import javax.jcr.Node; 025import javax.jcr.RepositoryException; 026import javax.jcr.Session; 027 028import org.apache.avalon.framework.service.ServiceException; 029import org.apache.avalon.framework.service.ServiceManager; 030import org.apache.commons.lang3.StringUtils; 031 032import org.ametys.cms.FilterNameHelper; 033import org.ametys.cms.ObservationConstants; 034import org.ametys.cms.repository.Content; 035import org.ametys.cms.repository.ModifiableContent; 036import org.ametys.cms.repository.ModifiableWorkflowAwareContent; 037import org.ametys.core.observation.Event; 038import org.ametys.core.observation.ObservationManager; 039import org.ametys.core.user.UserIdentity; 040import org.ametys.core.user.population.UserPopulationDAO; 041import org.ametys.plugins.repository.AmetysObjectResolver; 042import org.ametys.plugins.repository.AmetysRepositoryException; 043import org.ametys.plugins.repository.ModifiableTraversableAmetysObject; 044import org.ametys.plugins.repository.RepositoryConstants; 045import org.ametys.plugins.repository.RepositoryIntegrityViolationException; 046import org.ametys.plugins.repository.metadata.ModifiableCompositeMetadata; 047import org.ametys.plugins.workflow.store.AbstractJackrabbitWorkflowStore; 048import org.ametys.plugins.workflow.store.AmetysObjectWorkflowStore; 049 050import com.opensymphony.module.propertyset.PropertySet; 051import com.opensymphony.workflow.FunctionProvider; 052import com.opensymphony.workflow.WorkflowException; 053import com.opensymphony.workflow.spi.WorkflowEntry; 054import com.opensymphony.workflow.spi.WorkflowStore; 055 056/** 057 * OSWorkflow function for creating a content. 058 */ 059public class CreateContentFunction extends AbstractContentWorkflowComponent implements FunctionProvider 060{ 061 /** Constant for storing the content name to use into the transient variables map. */ 062 public static final String CONTENT_NAME_KEY = CreateContentFunction.class.getName() + "$contentName"; 063 /** Constant for storing the content title to use into the transient variables map. */ 064 public static final String CONTENT_TITLE_KEY = CreateContentFunction.class.getName() + "$contentTitle"; 065 /** Constant for storing the content type to use into the transient variables map. */ 066 public static final String CONTENT_TYPES_KEY = CreateContentFunction.class.getName() + "$contentTypes"; 067 /** Constant for storing the content type to use into the transient variables map. */ 068 public static final String CONTENT_MIXINS_KEY = CreateContentFunction.class.getName() + "$mixins"; 069 /** Constant for storing the content language to use into the transient variables map. */ 070 public static final String CONTENT_LANGUAGE_KEY = CreateContentFunction.class.getName() + "$contentLanguage"; 071 /** Constant for storing the content language to use into the transient variables map. */ 072 public static final String PARENT_CONTENT_ID_KEY = CreateContentFunction.class.getName() + "$parentContentId"; 073 /** Constant for storing the content language to use into the transient variables map. */ 074 public static final String PARENT_CONTENT_METADATA_PATH_KEY = CreateContentFunction.class.getName() + "$parentContentMetadataPath"; 075 076 /** Ametys object resolver available to subclasses. */ 077 protected AmetysObjectResolver _resolver; 078 /** Observation manager available to subclasses. */ 079 protected ObservationManager _observationManager; 080 081 @Override 082 public void service(ServiceManager manager) throws ServiceException 083 { 084 super.service(manager); 085 _resolver = (AmetysObjectResolver) manager.lookup(AmetysObjectResolver.ROLE); 086 _observationManager = (ObservationManager) manager.lookup(ObservationManager.ROLE); 087 } 088 089 @Override 090 public void execute(Map transientVars, Map args, PropertySet ps) throws WorkflowException 091 { 092 _logger.info("Performing content creation"); 093 094 try 095 { 096 String desiredContentName = _getNonNullVar(transientVars, CONTENT_NAME_KEY, "Missing content name"); 097 String contentTitle = _getNonNullVar(transientVars, CONTENT_TITLE_KEY, "Missing content title"); 098 099 String[] contentTypes = null; 100 101 if (transientVars.get(CONTENT_TYPES_KEY) != null) 102 { 103 contentTypes = (String[]) transientVars.get(CONTENT_TYPES_KEY); 104 } 105 else 106 { 107 throw new WorkflowException("Missing contents types"); 108 } 109 110 String[] mixins = new String[0]; 111 if (transientVars.get(CONTENT_MIXINS_KEY) != null) 112 { 113 mixins = (String[]) transientVars.get(CONTENT_MIXINS_KEY); 114 } 115 116 String contentLanguage = _getNonNullVar(transientVars, CONTENT_LANGUAGE_KEY, "Missing content language"); 117 118 ModifiableTraversableAmetysObject contents = _getContentRoot(transientVars); 119 120 ModifiableWorkflowAwareContent content = _createContent(transientVars, desiredContentName, contents); 121 122 content.setTitle(contentTitle); 123 content.setTypes(contentTypes); 124 content.setMixinTypes(mixins); 125 content.setLanguage(contentLanguage); 126 127 // Set the workflow id 128 long workflowId = ((WorkflowEntry) transientVars.get("entry")).getId(); 129 content.setWorkflowId(workflowId); 130 131 _populateContent(transientVars, content); 132 133 // FIXME previous statements may have failed. 134 contents.saveChanges(); 135 136 _populateAdditionalData(transientVars, content); 137 138 Node node = content.getNode(); 139 Session session = node.getSession(); 140 141 try 142 { 143 WorkflowStore workflowStore = (WorkflowStore) transientVars.get("store"); 144 145 if (workflowStore instanceof AmetysObjectWorkflowStore) 146 { 147 AmetysObjectWorkflowStore ametysObjectWorkflowStore = (AmetysObjectWorkflowStore) workflowStore; 148 ametysObjectWorkflowStore.bindAmetysObject(content); 149 } 150 151 if (workflowStore instanceof AbstractJackrabbitWorkflowStore) 152 { 153 AbstractJackrabbitWorkflowStore jackrabbitWorkflowStore = (AbstractJackrabbitWorkflowStore) workflowStore; 154 Node workflowEntryNode = jackrabbitWorkflowStore.getEntryNode(session, workflowId); 155 156 Integer actionId = (Integer) transientVars.get("actionId"); 157 if (actionId != null) 158 { 159 workflowEntryNode.setProperty("ametys-internal:initialActionId", actionId); 160 } 161 } 162 } 163 catch (RepositoryException e) 164 { 165 throw new AmetysRepositoryException("Unable to link the workflow to the content", e); 166 } 167 168 String parentContentId = (String) transientVars.get(PARENT_CONTENT_ID_KEY); 169 String parentContentMetadataPath = (String) transientVars.get(PARENT_CONTENT_METADATA_PATH_KEY); 170 if (StringUtils.isNotBlank(parentContentId) && StringUtils.isNotBlank(parentContentMetadataPath)) 171 { 172 node.setProperty("ametys-internal:subContent", true); 173 } 174 175 session.save(); 176 177 // Notify observers 178 _notifyContentAdded(content, transientVars); 179 180 // Content created 181 transientVars.put(CONTENT_KEY, content); 182 183 getResultsMap(transientVars).put("contentId", content.getId()); 184 getResultsMap(transientVars).put(CONTENT_KEY, content); 185 } 186 catch (RepositoryException e) 187 { 188 throw new WorkflowException("Unable to link the workflow to the content", e); 189 } 190 catch (AmetysRepositoryException e) 191 { 192 throw new WorkflowException("Unable to create the content", e); 193 } 194 } 195 196 /** 197 * Notify observers that the content has been created 198 * @param content The content added 199 * @param transientVars The workflow vars 200 * @return The {@link Future} objects of the asynchronous observers 201 * @throws WorkflowException If an error occurred 202 */ 203 protected List<Future> _notifyContentAdded(Content content, Map transientVars) throws WorkflowException 204 { 205 Map<String, Object> eventParams = new HashMap<>(); 206 eventParams.put(ObservationConstants.ARGS_CONTENT, content); 207 eventParams.put(ObservationConstants.ARGS_CONTENT_ID, content.getId()); 208 209 return _observationManager.notify(new Event(ObservationConstants.EVENT_CONTENT_ADDED, getUser(transientVars), eventParams)); 210 } 211 212 /** 213 * Create the content object. 214 * @param transientVars the workflow parameters. 215 * @param desiredContentName the desired content name. 216 * @param contentsNode the content root node in the repository. 217 * @return the created Content. 218 */ 219 protected ModifiableWorkflowAwareContent _createContent(Map transientVars, String desiredContentName, ModifiableTraversableAmetysObject contentsNode) 220 { 221 ModifiableWorkflowAwareContent content = null; 222 223 String contentName = FilterNameHelper.filterName(desiredContentName); 224 int errorCount = 0; 225 do 226 { 227 if (errorCount != 0) 228 { 229 contentName = FilterNameHelper.filterName(desiredContentName + " " + (errorCount + 1)); 230 } 231 try 232 { 233 String type = _getObjectType(transientVars); 234 content = contentsNode.createChild(contentName, type); 235 } 236 catch (RepositoryIntegrityViolationException e) 237 { 238 // Content name is already used 239 errorCount++; 240 } 241 } 242 while (content == null); 243 244 return content; 245 } 246 247 /** 248 * Return the type of the object to be created. 249 * Ex: ametys:defaultContent. 250 * @param transientVars The workflow vars 251 * @return The type of the object to be used during content creation. 252 */ 253 protected String _getObjectType(Map transientVars) 254 { 255 return RepositoryConstants.NAMESPACE_PREFIX + ":defaultContent"; 256 } 257 258 /** 259 * Retrieve the content root. 260 * @param transientVars the workflow parameters. 261 * @return the content root node. 262 * @throws WorkflowException if an error occurs 263 */ 264 protected ModifiableTraversableAmetysObject _getContentRoot(Map transientVars) throws WorkflowException 265 { 266 String parentContentId = (String) transientVars.get(PARENT_CONTENT_ID_KEY); 267 String parentContentMetadataPath = (String) transientVars.get(PARENT_CONTENT_METADATA_PATH_KEY); 268 269 if (StringUtils.isNotBlank(parentContentId) && StringUtils.isNotBlank(parentContentMetadataPath)) 270 { 271 return _getSubContentRoot(parentContentId, parentContentMetadataPath); 272 } 273 274 return _resolver.resolveByPath("/" + RepositoryConstants.NAMESPACE_PREFIX + ":contents"); 275 } 276 277 /** 278 * Get the content root when creating a sub-content. 279 * @param parentContentId the parent content ID. 280 * @param parentContentMetadataPath the path of the metadata in which to create the sub-content. 281 * @return the content collection metadata. 282 * @throws WorkflowException if an error occurs. 283 */ 284 protected ModifiableTraversableAmetysObject _getSubContentRoot(String parentContentId, String parentContentMetadataPath) throws WorkflowException 285 { 286 Content parentContent = _resolver.resolveById(parentContentId); 287 288 if (parentContent instanceof ModifiableContent) 289 { 290 ModifiableCompositeMetadata meta = ((ModifiableContent) parentContent).getMetadataHolder(); 291 292 String metaPath = parentContentMetadataPath.replace('/', '.'); 293 294 String[] metadatas = StringUtils.split(metaPath, '.'); 295 for (int i = 0; i < metadatas.length - 1; i++) 296 { 297 meta = meta.getCompositeMetadata(metadatas[i], true); 298 } 299 300 String metaName = metadatas[metadatas.length - 1]; 301 302 return meta.getObjectCollection(metaName, true); 303 } 304 else 305 { 306 throw new WorkflowException("The content " + parentContentId + " is not modifiable."); 307 } 308 } 309 310 /** 311 * Get a workflow parameter, throwing an exception if it is null. 312 * @param transientVars the workflow parameters. 313 * @param name the variable name. 314 * @param exceptionName label of the exception to throw if the variable is null. 315 * @return the variable value. 316 * @throws WorkflowException If an error occurred 317 */ 318 protected String _getNonNullVar(Map transientVars, String name, String exceptionName) throws WorkflowException 319 { 320 String value = (String) transientVars.get(name); 321 if (value == null) 322 { 323 throw new WorkflowException(exceptionName); 324 } 325 return value; 326 } 327 328 /** 329 * Populate the content. 330 * @param transientVars the transient variables. 331 * @param content the content. 332 * @throws WorkflowException if an error occurs. 333 */ 334 protected void _populateContent(Map transientVars, ModifiableContent content) throws WorkflowException 335 { 336 UserIdentity user = getUser(transientVars); 337 if (user == null) 338 { 339 // FIXME Login can be null when creating the content in the background environment. 340 user = getSystemUser(); 341 } 342 343 // Add standard metadata 344 content.setCreator(user); 345 content.setCreationDate(new Date()); 346 content.setLastContributor(user); 347 content.setLastModified(new Date()); 348 } 349 350 /** 351 * Get the user to use when the content is created in a background environment. 352 * @return The anonymous user 353 */ 354 protected UserIdentity getSystemUser () 355 { 356 return UserPopulationDAO.SYSTEM_USER_IDENTITY; 357 } 358 359 /** 360 * Populate the content. 361 * @param transientVars the transient variables. 362 * @param content the content. 363 * @throws WorkflowException if an error occurs. 364 */ 365 protected void _populateAdditionalData(Map transientVars, ModifiableContent content) throws WorkflowException 366 { 367 // Nothing to do. 368 } 369}