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):&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 */
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}