001    /**
002     * Copyright 2005-2013 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     */
016    package org.kuali.rice.krms.impl.peopleflow;
017    
018    import org.apache.commons.lang.StringUtils;
019    import org.kuali.rice.core.api.config.property.ConfigurationService;
020    import org.kuali.rice.core.api.exception.RiceIllegalArgumentException;
021    import org.kuali.rice.core.api.uif.DataType;
022    import org.kuali.rice.core.api.uif.RemotableAbstractWidget;
023    import org.kuali.rice.core.api.uif.RemotableAttributeError;
024    import org.kuali.rice.core.api.uif.RemotableAttributeField;
025    import org.kuali.rice.core.api.uif.RemotableAttributeLookupSettings;
026    import org.kuali.rice.core.api.uif.RemotableQuickFinder;
027    import org.kuali.rice.core.api.uif.RemotableTextInput;
028    import org.kuali.rice.core.api.util.jaxb.MapStringStringAdapter;
029    import org.kuali.rice.kew.api.KewApiServiceLocator;
030    import org.kuali.rice.kew.api.action.ActionRequestType;
031    import org.kuali.rice.kew.api.peopleflow.PeopleFlowDefinition;
032    import org.kuali.rice.kew.api.peopleflow.PeopleFlowService;
033    import org.kuali.rice.krad.uif.util.LookupInquiryUtils;
034    import org.kuali.rice.krms.api.engine.ExecutionEnvironment;
035    import org.kuali.rice.krms.api.repository.action.ActionDefinition;
036    import org.kuali.rice.krms.api.repository.type.KrmsAttributeDefinition;
037    import org.kuali.rice.krms.api.repository.type.KrmsTypeAttribute;
038    import org.kuali.rice.krms.framework.engine.Action;
039    import org.kuali.rice.krms.framework.type.ActionTypeService;
040    import org.kuali.rice.krms.impl.type.KrmsTypeServiceBase;
041    import org.springframework.orm.ObjectRetrievalFailureException;
042    
043    import javax.jws.WebParam;
044    import javax.xml.bind.annotation.adapters.XmlJavaTypeAdapter;
045    import java.text.MessageFormat;
046    import java.util.Collections;
047    import java.util.HashMap;
048    import java.util.List;
049    import 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):&lt;peopleFlowId&gt;{,(notification|approval):&lt;peopleFlowId&gt;}</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     */
063    public 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    }