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