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):&lt;peopleFlowId&gt;{,(notification|approval):&lt;peopleFlowId&gt;}</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}