001/** 002 * Copyright 2005-2017 The Kuali Foundation 003 * 004 * Licensed under the Educational Community 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.opensource.org/licenses/ecl2.php 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.kuali.rice.krms.impl.peopleflow; 017 018import java.text.MessageFormat; 019import java.util.Collections; 020import java.util.HashMap; 021import java.util.List; 022import java.util.Map; 023 024import javax.jws.WebParam; 025import javax.xml.bind.annotation.adapters.XmlJavaTypeAdapter; 026 027import org.apache.commons.lang.StringUtils; 028import org.kuali.rice.core.api.config.property.ConfigurationService; 029import org.kuali.rice.core.api.data.DataType; 030import org.kuali.rice.core.api.exception.RiceIllegalArgumentException; 031import org.kuali.rice.core.api.uif.RemotableAbstractWidget; 032import org.kuali.rice.core.api.uif.RemotableAttributeError; 033import org.kuali.rice.core.api.uif.RemotableAttributeField; 034import org.kuali.rice.core.api.uif.RemotableAttributeLookupSettings; 035import org.kuali.rice.core.api.uif.RemotableQuickFinder; 036import org.kuali.rice.core.api.uif.RemotableTextInput; 037import org.kuali.rice.core.api.util.jaxb.MapStringStringAdapter; 038import org.kuali.rice.kew.api.KewApiServiceLocator; 039import org.kuali.rice.kew.api.action.ActionRequestType; 040import org.kuali.rice.kew.api.peopleflow.PeopleFlowDefinition; 041import org.kuali.rice.kew.api.peopleflow.PeopleFlowService; 042import org.kuali.rice.krad.lookup.LookupUtils; 043import org.kuali.rice.krms.api.engine.ExecutionEnvironment; 044import org.kuali.rice.krms.api.repository.action.ActionDefinition; 045import org.kuali.rice.krms.api.repository.type.KrmsAttributeDefinition; 046import org.kuali.rice.krms.api.repository.type.KrmsTypeAttribute; 047import org.kuali.rice.krms.framework.engine.Action; 048import org.kuali.rice.krms.framework.type.ActionTypeService; 049import org.kuali.rice.krms.impl.type.KrmsTypeServiceBase; 050import org.springframework.orm.ObjectRetrievalFailureException; 051 052/** 053 * <p>{@link ActionTypeService} implementation for PeopleFlow actions. The loaded {@link Action}s will place or extend 054 * an attribute in the {@link org.kuali.rice.krms.api.engine.EngineResults} whose key is "peopleFlowSelected" and value 055 * is a String of the form (using EBNF-like notation):</p> 056 * 057 * <pre> (notification|approval):<peopleFlowId>{,(notification|approval):<peopleFlowId>}</pre> 058 * 059 * <p>An example value with two people flow actions specified would be:</p> 060 * 061 * <pre> "A:1000,F:1001"</pre> 062 * 063 */ 064public class PeopleFlowActionTypeService extends KrmsTypeServiceBase implements ActionTypeService { 065 066 // TODO: where should this constant really go? 067 static final String PEOPLE_FLOW_BO_CLASS_NAME = "org.kuali.rice.kew.impl.peopleflow.PeopleFlowBo"; 068 069 /** 070 * enum used to specify the action type to be specified in the vended actions. 071 */ 072 public enum Type { 073 074 /** 075 * use this flag with the static factory to get a {@link PeopleFlowActionTypeService} that creates 076 * notification actions. 077 */ 078 NOTIFICATION(ActionRequestType.FYI), 079 080 /** 081 * use this flag with the static factory to get a {@link PeopleFlowActionTypeService} that creates 082 * approval actions. 083 */ 084 APPROVAL(ActionRequestType.APPROVE); 085 086 private final ActionRequestType actionRequestType; 087 088 private Type(ActionRequestType actionRequestType) { 089 this.actionRequestType = actionRequestType; 090 } 091 092 @Override 093 public String toString() { 094 return this.name().toLowerCase(); 095 } 096 097 /** 098 * 099 * @return {@link ActionRequestType} 100 */ 101 public ActionRequestType getActionRequestType() { 102 return this.actionRequestType; 103 } 104 105 /** 106 * for each type, check the input with the lowercase version of the type name, and returns any match. 107 * @param s the type to retrieve 108 * @return the type, or null if a match is not found. 109 */ 110 public static Type fromString(String s) { 111 for (Type type : Type.values()) { 112 if (type.toString().equals(s.toLowerCase())) { 113 return type; 114 } 115 } 116 return null; 117 } 118 } 119 120 // String constants 121 static final String PEOPLE_FLOWS_SELECTED_ATTRIBUTE = "peopleFlowsSelected"; 122 public static final String ATTRIBUTE_FIELD_NAME = "peopleFlowId"; 123 public static final String NAME_ATTRIBUTE_FIELD = "peopleFlowName"; 124 125 private final Type type; 126 127 private PeopleFlowService peopleFlowService; 128 private ConfigurationService configurationService; 129 130 /** 131 * Factory method for getting a {@link PeopleFlowActionTypeService} 132 * @param type indicates the type of action that the returned {@link PeopleFlowActionTypeService} will produce 133 * @return a {@link PeopleFlowActionTypeService} corresponding to the given {@link Type}. 134 */ 135 public static PeopleFlowActionTypeService getInstance(Type type) { 136 return new PeopleFlowActionTypeService(type); 137 } 138 139 /** 140 * Constructs a PeopleFlowActionTypeService of the given Type 141 * @param type required 142 * @throws IllegalArgumentException if type is null 143 */ 144 public PeopleFlowActionTypeService(Type type) { 145 if (type == null) { throw new IllegalArgumentException("type must not be null"); } 146 this.type = type; 147 } 148 149 /** 150 * inject the {@link ConfigurationService} to use internally. 151 * @param configurationService 152 */ 153 public void setConfigurationService(ConfigurationService configurationService) { 154 this.configurationService = configurationService; 155 } 156 157 /** 158 * 159 * @param actionDefinition 160 * @return {@link Action} as defined by the given {@link ActionDefinition} 161 * @throws RiceIllegalArgumentException is actionDefinition is null, attributes do not contain the ATTRIBUTE_FIELD_NAME key, 162 * or the NAME_ATTRIBUTE_FIELD key. 163 * 164 */ 165 @Override 166 public Action loadAction(ActionDefinition actionDefinition) { 167 if (actionDefinition == null) { throw new RiceIllegalArgumentException("actionDefinition must not be null"); } 168 169 if (actionDefinition.getAttributes() == null || 170 !actionDefinition.getAttributes().containsKey(ATTRIBUTE_FIELD_NAME)) { 171 172 throw new RiceIllegalArgumentException("actionDefinition does not contain an " + 173 ATTRIBUTE_FIELD_NAME + " attribute"); 174 } 175 176 String peopleFlowId = actionDefinition.getAttributes().get(ATTRIBUTE_FIELD_NAME); 177 if (StringUtils.isBlank(peopleFlowId)) { 178 throw new RiceIllegalArgumentException(ATTRIBUTE_FIELD_NAME + " attribute must not be null or blank"); 179 } 180 181 // if the ActionDefinition is valid, constructing the PeopleFlowAction is cake 182 return new PeopleFlowAction(type, peopleFlowId); 183 } 184 185 @Override 186 public RemotableAttributeField translateTypeAttribute(KrmsTypeAttribute inputAttribute, 187 KrmsAttributeDefinition attributeDefinition) { 188 189 if (ATTRIBUTE_FIELD_NAME.equals(attributeDefinition.getName())) { 190 return createPeopleFlowIdField(); 191 } else if (NAME_ATTRIBUTE_FIELD.equals(attributeDefinition.getName())) { 192 return createPeopleFlowNameField(); 193 } else { 194 return super.translateTypeAttribute(inputAttribute, 195 attributeDefinition); 196 } 197 } 198 199 /** 200 * Create the PeopleFlow Id input field 201 * @return RemotableAttributeField 202 */ 203 private RemotableAttributeField createPeopleFlowIdField() { 204 205 String baseLookupUrl = LookupUtils.getBaseLookupUrl(); 206 207 RemotableQuickFinder.Builder quickFinderBuilder = 208 RemotableQuickFinder.Builder.create(baseLookupUrl, PEOPLE_FLOW_BO_CLASS_NAME); 209 Map<String, String> lookup = new HashMap<String, String>(); 210 lookup.put(ATTRIBUTE_FIELD_NAME, "id"); 211 quickFinderBuilder.setLookupParameters(lookup); 212 213 Map<String,String> fieldConversions = new HashMap<String, String>(); 214 fieldConversions.put("id", ATTRIBUTE_FIELD_NAME); 215 fieldConversions.put("name", NAME_ATTRIBUTE_FIELD); 216 217 quickFinderBuilder.setFieldConversions(fieldConversions); 218 219 RemotableTextInput.Builder controlBuilder = RemotableTextInput.Builder.create(); 220 controlBuilder.setSize(Integer.valueOf(40)); 221 controlBuilder.setWatermark("PeopleFlow ID"); 222 223 RemotableAttributeLookupSettings.Builder lookupSettingsBuilder = RemotableAttributeLookupSettings.Builder.create(); 224 lookupSettingsBuilder.setCaseSensitive(Boolean.TRUE); 225 lookupSettingsBuilder.setInCriteria(true); 226 lookupSettingsBuilder.setInResults(true); 227 lookupSettingsBuilder.setRanged(false); 228 229 RemotableAttributeField.Builder builder = RemotableAttributeField.Builder.create(ATTRIBUTE_FIELD_NAME); 230 builder.setAttributeLookupSettings(lookupSettingsBuilder); 231 builder.setRequired(true); 232 builder.setDataType(DataType.STRING); 233 builder.setControl(controlBuilder); 234 builder.setLongLabel("PeopleFlow ID"); 235 builder.setShortLabel("PeopleFlow ID"); 236 builder.setMinLength(Integer.valueOf(1)); 237 builder.setMaxLength(Integer.valueOf(40)); 238 builder.setConstraintText("size 40"); 239 builder.setWidgets(Collections.<RemotableAbstractWidget.Builder>singletonList(quickFinderBuilder)); 240 241 return builder.build(); 242 } 243 244 /** 245 * Create the PeopleFlow Name input field 246 * @return RemotableAttributeField 247 */ 248 private RemotableAttributeField createPeopleFlowNameField() { 249 250 String baseLookupUrl = LookupUtils.getBaseLookupUrl(); 251 252 RemotableTextInput.Builder controlBuilder = RemotableTextInput.Builder.create(); 253 controlBuilder.setSize(Integer.valueOf(40)); 254 controlBuilder.setWatermark("PeopleFlow Name"); 255 256 RemotableAttributeField.Builder builder = RemotableAttributeField.Builder.create(NAME_ATTRIBUTE_FIELD); 257 builder.setRequired(true); 258 builder.setDataType(DataType.STRING); 259 builder.setControl(controlBuilder); 260 builder.setLongLabel("PeopleFlow Name"); 261 builder.setShortLabel("PeopleFlow Name"); 262 builder.setMinLength(Integer.valueOf(1)); 263 builder.setMaxLength(Integer.valueOf(40)); 264 builder.setConstraintText("size 40"); 265 266 return builder.build(); 267 } 268 269 /** 270 * Validate that the krmsTypeId is not null or blank 271 * @param krmsTypeId to validate 272 * @throws RiceIllegalArgumentException if krmsTypeId is null or blank 273 */ 274 private void validateNonBlankKrmsTypeId(String krmsTypeId) { 275 if (StringUtils.isEmpty(krmsTypeId)) { 276 throw new RiceIllegalArgumentException("krmsTypeId may not be null or blank"); 277 } 278 } 279 280 /** 281 * Attributes must include a ATTRIBUTE_FIELD_NAME 282 * @param krmsTypeId the people flow type identifier. Must not be null or blank. 283 * @param attributes the attributes to validate. Cannot be null. 284 * @return 285 * @throws RiceIllegalArgumentException if required attribute ATTRIBUTE_FIELD_NAME is not in the given attributes 286 */ 287 @Override 288 public List<RemotableAttributeError> validateAttributes( 289 290 @WebParam(name = "krmsTypeId") String krmsTypeId, 291 292 @WebParam(name = "attributes") 293 @XmlJavaTypeAdapter(value = MapStringStringAdapter.class) 294 Map<String, String> attributes 295 296 ) throws RiceIllegalArgumentException { 297 298 List<RemotableAttributeError> results = null; 299 300 validateNonBlankKrmsTypeId(krmsTypeId); 301 if (attributes == null) { throw new RiceIllegalArgumentException("attributes must not be null"); } 302 303 RemotableAttributeError.Builder errorBuilder = 304 RemotableAttributeError.Builder.create(ATTRIBUTE_FIELD_NAME); 305 306 if (attributes != null && attributes.containsKey(ATTRIBUTE_FIELD_NAME) && StringUtils.isNotBlank(attributes.get(ATTRIBUTE_FIELD_NAME))) { 307 PeopleFlowDefinition peopleFlowDefinition = null; 308 309 try { 310 peopleFlowDefinition = getPeopleFlowService().getPeopleFlow(attributes.get(ATTRIBUTE_FIELD_NAME)); 311 } catch (ObjectRetrievalFailureException e) { 312 // that means the key was invalid to OJB/Spring. 313 // That's not cause for general panic, so we'll swallow it. 314 } catch (IllegalArgumentException e) { 315 // that means the key was invalid to our JPA provider. 316 // That's not cause for general panic, so we'll swallow it. 317 } 318 319 if (peopleFlowDefinition == null) { 320 // TODO: include the ATTRIBUTE_FIELD_NAME in an error message like 321 // "The " + ATTRIBUTE_FIELD_NAME + " must be a valid ID for an existing PeopleFlow". 322 // Currently the RemotableAttributeError doesn't support arguments in the error messages. 323 errorBuilder.addErrors(MessageFormat.format(configurationService.getPropertyValueAsString("peopleFlow.peopleFlowId.invalid"), ATTRIBUTE_FIELD_NAME)); 324 } 325 } else { 326 // TODO: include the ATTRIBUTE_FIELD_NAME in an error message like 327 // ATTRIBUTE_FIELD_NAME + " is required". 328 // Currently the RemotableAttributeError doesn't support arguments in the error messages. 329 errorBuilder.addErrors(MessageFormat.format(configurationService.getPropertyValueAsString("peopleFlow.peopleFlowId.required"), ATTRIBUTE_FIELD_NAME)); 330 } 331 332 if (errorBuilder.getErrors().size() > 0) { 333 results = Collections.singletonList(errorBuilder.build()); 334 } else { 335 results = Collections.emptyList(); 336 } 337 338 return results; 339 } 340 341 342 @Override 343 public List<RemotableAttributeError> validateAttributesAgainstExisting( 344 @WebParam(name = "krmsTypeId") String krmsTypeId, @WebParam(name = "newAttributes") @XmlJavaTypeAdapter( 345 value = MapStringStringAdapter.class) Map<String, String> newAttributes, 346 @WebParam(name = "oldAttributes") @XmlJavaTypeAdapter( 347 value = MapStringStringAdapter.class) Map<String, String> oldAttributes) throws RiceIllegalArgumentException { 348 349 if (oldAttributes == null) { throw new RiceIllegalArgumentException("oldAttributes must not be null"); } 350 351 return validateAttributes(krmsTypeId, newAttributes); 352 } 353 354 /** 355 * @return the configured {@link PeopleFlowService} */ 356 public PeopleFlowService getPeopleFlowService() { 357 if (peopleFlowService == null) { 358 peopleFlowService = KewApiServiceLocator.getPeopleFlowService(); 359 } 360 361 return peopleFlowService; 362 } 363 364 /** 365 * inject the {@link PeopleFlowService} to use internally. 366 * @param peopleFlowService 367 */ 368 public void setPeopleFlowService(PeopleFlowService peopleFlowService) { 369 this.peopleFlowService = peopleFlowService; 370 } 371 372 private static class PeopleFlowAction implements Action { 373 374 private final Type type; 375 private final String peopleFlowId; 376 377 private PeopleFlowAction(Type type, String peopleFlowId) { 378 379 if (type == null) throw new IllegalArgumentException("type must not be null"); 380 if (StringUtils.isBlank(peopleFlowId)) throw new IllegalArgumentException("peopleFlowId must not be null or blank"); 381 382 this.type = type; 383 this.peopleFlowId = peopleFlowId; 384 } 385 386 @Override 387 public void execute(ExecutionEnvironment environment) { 388 // create or extend an existing attribute on the EngineResults to communicate the selected PeopleFlow and 389 // action 390 391 Object value = environment.getEngineResults().getAttribute(PEOPLE_FLOWS_SELECTED_ATTRIBUTE); 392 StringBuilder selectedAttributesStringBuilder = new StringBuilder(); 393 394 if (value != null) { 395 // assume the value is what we think it is 396 selectedAttributesStringBuilder.append(value.toString()); 397 // we need a comma after the initial value 398 selectedAttributesStringBuilder.append(","); 399 } 400 401 // add our people flow action to the string using our convention 402 selectedAttributesStringBuilder.append(type.getActionRequestType().getCode()); 403 selectedAttributesStringBuilder.append(":"); 404 selectedAttributesStringBuilder.append(peopleFlowId); 405 406 // set our attribute on the engine results 407 environment.getEngineResults().setAttribute( 408 PEOPLE_FLOWS_SELECTED_ATTRIBUTE, selectedAttributesStringBuilder.toString() 409 ); 410 } 411 412 @Override 413 public void executeSimulation(ExecutionEnvironment environment) { 414 // our action doesn't need special handling during simulations 415 execute(environment); 416 } 417 } 418}