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.rule;
017
018import org.apache.commons.collections.CollectionUtils;
019import org.apache.commons.lang.StringUtils;
020import org.kuali.rice.core.api.uif.RemotableAttributeError;
021import org.kuali.rice.core.api.util.RiceKeyConstants;
022import org.kuali.rice.krad.bo.GlobalBusinessObject;
023import org.kuali.rice.krad.bo.PersistableBusinessObject;
024import org.kuali.rice.krad.maintenance.MaintenanceDocument;
025import org.kuali.rice.krad.rules.MaintenanceDocumentRuleBase;
026import org.kuali.rice.krad.util.KRADConstants;
027import org.kuali.rice.krms.api.KrmsConstants;
028import org.kuali.rice.krms.api.repository.agenda.AgendaDefinition;
029import org.kuali.rice.krms.api.repository.rule.RuleDefinition;
030import org.kuali.rice.krms.api.repository.type.KrmsTypeDefinition;
031import org.kuali.rice.krms.api.repository.type.KrmsTypeRepositoryService;
032import org.kuali.rice.krms.framework.type.ActionTypeService;
033import org.kuali.rice.krms.framework.type.AgendaTypeService;
034import org.kuali.rice.krms.impl.authorization.AgendaAuthorizationService;
035import org.kuali.rice.krms.impl.repository.ActionBo;
036import org.kuali.rice.krms.impl.repository.AgendaBo;
037import org.kuali.rice.krms.impl.repository.AgendaBoService;
038import org.kuali.rice.krms.impl.repository.AgendaItemBo;
039import org.kuali.rice.krms.impl.repository.ContextBoService;
040import org.kuali.rice.krms.impl.repository.KrmsRepositoryServiceLocator;
041import org.kuali.rice.krms.impl.repository.RuleBo;
042import org.kuali.rice.krms.impl.repository.RuleBoService;
043import org.kuali.rice.krms.impl.ui.AgendaEditor;
044import org.kuali.rice.krms.impl.util.KRMSPropertyConstants;
045
046import java.util.List;
047import java.util.Map;
048
049/**
050 * This class contains the rules for the AgendaEditor.
051 */
052public class AgendaEditorBusRule extends MaintenanceDocumentRuleBase {
053
054    @Override
055    protected boolean primaryKeyCheck(MaintenanceDocument document) {
056        // default to success if no failures
057        boolean success = true;
058        Class<?> dataObjectClass = document.getNewMaintainableObject().getDataObjectClass();
059
060        // Since the dataObject is a wrapper class we need to return the agendaBo instead.
061        Object oldBo = ((AgendaEditor) document.getOldMaintainableObject().getDataObject()).getAgenda();
062        Object newDataObject = ((AgendaEditor) document.getNewMaintainableObject().getDataObject()).getAgenda();
063
064        // We dont do primaryKeyChecks on Global Business Object maintenance documents. This is
065        // because it doesnt really make any sense to do so, given the behavior of Globals. When a
066        // Global Document completes, it will update or create a new record for each BO in the list.
067        // As a result, there's no problem with having existing BO records in the system, they will
068        // simply get updated.
069        if (newDataObject instanceof GlobalBusinessObject) {
070            return success;
071        }
072
073        // fail and complain if the person has changed the primary keys on
074        // an EDIT maintenance document.
075        if (document.isEdit()) {
076            if (!getDataObjectMetaDataService().equalsByPrimaryKeys(oldBo, newDataObject)) {
077                // add a complaint to the errors
078                putDocumentError(KRADConstants.DOCUMENT_ERRORS,
079                        RiceKeyConstants.ERROR_DOCUMENT_MAINTENANCE_PRIMARY_KEYS_CHANGED_ON_EDIT,
080                        getHumanReadablePrimaryKeyFieldNames(dataObjectClass));
081                success &= false;
082            }
083        }
084
085        // fail and complain if the person has selected a new object with keys that already exist
086        // in the DB.
087        else if (document.isNew()) {
088
089            // TODO: when/if we have standard support for DO retrieval, do this check for DO's
090            if (newDataObject instanceof PersistableBusinessObject) {
091
092                // get a map of the pk field names and values
093                Map<String, ?> newPkFields = getDataObjectMetaDataService().getPrimaryKeyFieldValues(newDataObject);
094
095                // TODO: Good suggestion from Aaron, dont bother checking the DB, if all of the
096                // objects PK fields dont have values. If any are null or empty, then
097                // we're done. The current way wont fail, but it will make a wasteful
098                // DB call that may not be necessary, and we want to minimize these.
099
100                // attempt to do a lookup, see if this object already exists by these Primary Keys
101                PersistableBusinessObject testBo = getBoService()
102                        .findByPrimaryKey(dataObjectClass.asSubclass(PersistableBusinessObject.class), newPkFields);
103
104                // if the retrieve was successful, then this object already exists, and we need
105                // to complain
106                if (testBo != null) {
107                    putDocumentError(KRADConstants.DOCUMENT_ERRORS,
108                            RiceKeyConstants.ERROR_DOCUMENT_MAINTENANCE_KEYS_ALREADY_EXIST_ON_CREATE_NEW,
109                            getHumanReadablePrimaryKeyFieldNames(dataObjectClass));
110                    success &= false;
111                }
112            }
113        }
114
115        return success;
116    }
117
118
119
120    @Override
121    protected boolean processCustomSaveDocumentBusinessRules(MaintenanceDocument document) {
122        boolean isValid = true;
123
124        AgendaEditor agendaEditor = (AgendaEditor) document.getNewMaintainableObject().getDataObject();
125        AgendaEditor oldAgendaEditor = (AgendaEditor) document.getOldMaintainableObject().getDataObject();
126        isValid &= validContext(agendaEditor);
127        isValid &= validAgendaName(agendaEditor);
128        isValid &= validContextAgendaNamespace(agendaEditor);
129        isValid &= validAgendaTypeAndAttributes(oldAgendaEditor, agendaEditor);
130
131        return isValid;
132    }
133
134    /**
135     * Check if the context exists and if user has authorization to edit agendas under this context.
136     * @param agendaEditor
137     * @return true if the context exist and has authorization, false otherwise
138     */
139    public boolean validContext(AgendaEditor agendaEditor) {
140        boolean isValid = true;
141
142        try {
143            if (getContextBoService().getContextByContextId(agendaEditor.getAgenda().getContextId()) == null) {
144                this.putFieldError(KRMSPropertyConstants.Agenda.CONTEXT, "error.agenda.invalidContext");
145                isValid = false;
146            } else {
147                if (!getAgendaAuthorizationService().isAuthorized(KrmsConstants.MAINTAIN_KRMS_AGENDA,
148                        agendaEditor.getAgenda().getContextId())) {
149                    this.putFieldError(KRMSPropertyConstants.Agenda.CONTEXT, "error.agenda.unauthorizedContext");
150                    isValid = false;
151                }
152            }
153        }
154        catch (IllegalArgumentException e) {
155            this.putFieldError(KRMSPropertyConstants.Agenda.CONTEXT, "error.agenda.invalidContext");
156            isValid = false;
157        }
158
159        return isValid;
160    }
161
162    /**
163     * Check if for namespace.
164     * @param agendaEditor
165     * @return
166     */
167    public boolean validContextAgendaNamespace(AgendaEditor agendaEditor) {
168        if (StringUtils.isNotBlank(agendaEditor.getNamespace()) &&
169                getContextBoService().getContextByNameAndNamespace(agendaEditor.getContextName(), agendaEditor.getNamespace()) != null) {
170            return true;
171        } else {
172            this.putFieldError(KRMSPropertyConstants.Context.NAMESPACE, "error.context.invalidNamespace");
173            return false;
174        }
175    }
176
177    private boolean validAgendaTypeAndAttributes( AgendaEditor oldAgendaEditor, AgendaEditor newAgendaEditor) {
178        if (validAgendaType(newAgendaEditor.getAgenda().getTypeId(), newAgendaEditor.getAgenda().getContextId())) {
179            return validAgendaAttributes(oldAgendaEditor, newAgendaEditor);
180        } else {
181            return false;
182        }
183    }
184    private boolean validAgendaType(String typeId, String contextId) {
185        boolean isValid = true;
186
187        if (!StringUtils.isBlank(typeId) && !StringUtils.isBlank(contextId)) {
188            if (getKrmsTypeRepositoryService().getAgendaTypeByAgendaTypeIdAndContextId(typeId, contextId) != null) {
189                return true;
190            } else {
191                this.putFieldError(KRMSPropertyConstants.Agenda.TYPE, "error.agenda.invalidType");
192                return false;
193            }
194        }
195
196        return isValid;
197    }
198
199    private boolean validAgendaAttributes(AgendaEditor oldAgendaEditor, AgendaEditor newAgendaEditor) {
200        boolean isValid = true;
201
202        String typeId = newAgendaEditor.getAgenda().getTypeId();
203
204        if (!StringUtils.isEmpty(typeId)) {
205            KrmsTypeDefinition typeDefinition = getKrmsTypeRepositoryService().getTypeById(typeId);
206
207            if (typeDefinition == null) {
208                throw new IllegalStateException("agenda typeId must match the id of a valid krms type");
209            } else if (StringUtils.isBlank(typeDefinition.getServiceName())) {
210                throw new IllegalStateException("agenda type definition must have a non-blank service name");
211            } else {
212                AgendaTypeService agendaTypeService =
213                        (AgendaTypeService)KrmsRepositoryServiceLocator.getService(typeDefinition.getServiceName());
214
215                if (agendaTypeService == null) {
216                    throw new IllegalStateException("typeDefinition must have a valid serviceName");
217                } else {
218
219                    List<RemotableAttributeError> errors;
220                    if (oldAgendaEditor == null) {
221                        errors = agendaTypeService.validateAttributes(typeId, newAgendaEditor.getCustomAttributesMap());
222                    } else {
223                        errors = agendaTypeService.validateAttributesAgainstExisting(typeId, newAgendaEditor.getCustomAttributesMap(), oldAgendaEditor.getCustomAttributesMap());
224                    }
225
226                    if (!CollectionUtils.isEmpty(errors)) {
227                        isValid = false;
228                        for (RemotableAttributeError error : errors) {
229                            for (String errorStr : error.getErrors()) {
230                                this.putFieldError(
231                                        KRMSPropertyConstants.AgendaEditor.CUSTOM_ATTRIBUTES_MAP +
232                                                "['" + error.getAttributeName() + "']",
233                                        errorStr
234                                );
235                            }
236                        }
237                    }
238                }
239            }
240        }
241        return isValid;
242    }
243
244    /**
245     * Check if an agenda with that name exists already in the context.
246     * @param agendaEditor
247     * @return true if agenda name is unique, false otherwise
248     */
249    public boolean validAgendaName(AgendaEditor agendaEditor) {
250        try {
251            AgendaDefinition agendaFromDataBase = getAgendaBoService().getAgendaByNameAndContextId(
252                    agendaEditor.getAgenda().getName(), agendaEditor.getAgenda().getContextId());
253            if ((agendaFromDataBase != null) && !StringUtils.equals(agendaFromDataBase.getId(), agendaEditor.getAgenda().getId())) {
254                this.putFieldError(KRMSPropertyConstants.Agenda.NAME, "error.agenda.duplicateName");
255                return false;
256            }
257        }
258        catch (IllegalArgumentException e) {
259            this.putFieldError(KRMSPropertyConstants.Agenda.NAME, "error.agenda.invalidName");
260            return false;
261        }
262        return true;
263    }
264
265    /**
266     * Check if a agenda item is valid.
267     *
268     * @param document, the Agenda document of the added/edited agenda item
269     * @return true if agenda item is valid, false otherwise
270     */
271    public boolean processAgendaItemBusinessRules(MaintenanceDocument document) {
272        boolean isValid = true;
273
274        AgendaEditor newAgendaEditor = (AgendaEditor) document.getNewMaintainableObject().getDataObject();
275        AgendaEditor oldAgendaEditor = (AgendaEditor) document.getOldMaintainableObject().getDataObject();
276        RuleBo rule = newAgendaEditor.getAgendaItemLine().getRule();
277        isValid &= validateRuleName(rule, newAgendaEditor.getAgenda());
278        isValid &= validRuleType(rule.getTypeId(), newAgendaEditor.getAgenda().getContextId());
279        isValid &= validateRuleAction(oldAgendaEditor, newAgendaEditor);
280
281        return isValid;
282    }
283
284    /**
285     * Check if a rule with that name exists already in the namespace.
286     * @param rule
287     * @parm agenda
288     * @return true if rule name is unique, false otherwise
289     */
290    private boolean validateRuleName(RuleBo rule, AgendaBo agenda) {
291        if (StringUtils.isBlank(rule.getName())) {
292            this.putFieldError(KRMSPropertyConstants.Rule.NAME, "error.rule.invalidName");
293            return false;
294        }
295        // check current bo for rules (including ones that aren't persisted to the database)
296        for (AgendaItemBo agendaItem : agenda.getItems()) {
297            if (!StringUtils.equals(agendaItem.getRule().getId(), rule.getId()) && StringUtils.equals(agendaItem.getRule().getName(), rule.getName())
298                    && StringUtils.equals(agendaItem.getRule().getNamespace(), rule.getNamespace())) {
299                this.putFieldError(KRMSPropertyConstants.Rule.NAME, "error.rule.duplicateName");
300                return false;
301            }
302        }
303
304        // check database for rules used with other agendas - the namespace might not yet be specified on new agendas.
305        if (StringUtils.isNotBlank(rule.getNamespace())) {
306            RuleDefinition ruleFromDatabase = getRuleBoService().getRuleByNameAndNamespace(rule.getName(), rule.getNamespace());
307            try {
308                if ((ruleFromDatabase != null) && !StringUtils.equals(ruleFromDatabase.getId(), rule.getId())) {
309                    this.putFieldError(KRMSPropertyConstants.Rule.NAME, "error.rule.duplicateName");
310                    return false;
311                }
312            }
313            catch (IllegalArgumentException e) {
314                this.putFieldError(KRMSPropertyConstants.Rule.NAME, "error.rule.invalidName");
315                return false;
316            }
317        }
318        return true;
319    }
320
321    /**
322     * Check that the rule type is valid when specified.
323     * @param ruleTypeId, the type id
324     * @param contextId, the contextId the action needs to belong to.
325     * @return true if valid, false otherwise.
326     */
327    private boolean validRuleType(String ruleTypeId, String contextId) {
328        if (StringUtils.isBlank(ruleTypeId)) {
329            return true;
330        }
331
332        if (getKrmsTypeRepositoryService().getRuleTypeByRuleTypeIdAndContextId(ruleTypeId, contextId) != null) {
333            return true;
334        } else {
335            this.putFieldError(KRMSPropertyConstants.Rule.TYPE, "error.rule.invalidType");
336            return false;
337        }
338    }
339
340    private boolean validateRuleAction(AgendaEditor oldAgendaEditor, AgendaEditor newAgendaEditor) {
341        boolean isValid = true;
342        ActionBo newActionBo = newAgendaEditor.getAgendaItemLineRuleAction();
343
344        isValid &= validRuleActionType(newActionBo.getTypeId(), newAgendaEditor.getAgenda().getContextId());
345        if (isValid && StringUtils.isNotBlank(newActionBo.getTypeId())) {
346            isValid &= validRuleActionName(newActionBo.getName());
347            isValid &= validRuleActionAttributes(oldAgendaEditor, newAgendaEditor);
348        }
349        return isValid;
350    }
351
352    /**
353     * Check that the rule action type is valid when specified.
354     * @param typeId, the action type id
355     * @parm contextId, the contextId the action needs to belong to.
356     * @return true if valid, false otherwise.
357     */
358    private boolean validRuleActionType(String typeId, String contextId) {
359        if (StringUtils.isBlank(typeId)) {
360            return true;
361        }
362
363        if (getKrmsTypeRepositoryService().getActionTypeByActionTypeIdAndContextId(typeId, contextId) != null) {
364            return true;
365        } else {
366            this.putFieldError(KRMSPropertyConstants.Action.TYPE, "error.action.invalidType");
367            return false;
368        }
369    }
370
371    /**
372     * Check that a action name is specified.
373     */
374    private boolean validRuleActionName(String name) {
375        if (StringUtils.isNotBlank(name)) {
376            return true;
377        } else {
378            this.putFieldError(KRMSPropertyConstants.Action.NAME, "error.action.missingName");
379            return false;
380        }
381    }
382
383    private boolean validRuleActionAttributes(AgendaEditor oldAgendaEditor, AgendaEditor newAgendaEditor) {
384        boolean isValid = true;
385
386        String typeId = newAgendaEditor.getAgendaItemLineRuleAction().getTypeId();
387
388        if (!StringUtils.isBlank(typeId)) {
389            KrmsTypeDefinition typeDefinition = getKrmsTypeRepositoryService().getTypeById(typeId);
390
391            if (typeDefinition == null) {
392                throw new IllegalStateException("rule action typeId must match the id of a valid krms type");
393            } else if (StringUtils.isBlank(typeDefinition.getServiceName())) {
394                throw new IllegalStateException("rule action type definition must have a non-blank service name");
395            } else {
396                ActionTypeService actionTypeService = getActionTypeService(typeDefinition.getServiceName());
397
398                if (actionTypeService == null) {
399                    throw new IllegalStateException("typeDefinition must have a valid serviceName");
400                } else {
401
402                    List<RemotableAttributeError> errors;
403                    if (oldAgendaEditor == null) {
404                        errors = actionTypeService.validateAttributes(typeId,
405                                newAgendaEditor.getCustomRuleActionAttributesMap());
406                    } else {
407                        errors = actionTypeService.validateAttributesAgainstExisting(typeId,
408                                newAgendaEditor.getCustomRuleActionAttributesMap(), oldAgendaEditor.getCustomRuleActionAttributesMap());
409                    }
410
411                    if (!CollectionUtils.isEmpty(errors)) {
412                        isValid = false;
413                        for (RemotableAttributeError error : errors) {
414                            for (String errorStr : error.getErrors()) {
415                                this.putFieldError(
416                                        KRMSPropertyConstants.AgendaEditor.CUSTOM_RULE_ACTION_ATTRIBUTES_MAP +
417                                                "['" + error.getAttributeName() + "']",
418                                        errorStr
419                                );
420                            }
421                        }
422                    }
423                }
424            }
425        }
426        return isValid;
427    }
428
429    public ContextBoService getContextBoService() {
430        return KrmsRepositoryServiceLocator.getContextBoService();
431    }
432
433    public AgendaBoService getAgendaBoService() {
434        return KrmsRepositoryServiceLocator.getAgendaBoService();
435    }
436
437    public RuleBoService getRuleBoService() {
438        return KrmsRepositoryServiceLocator.getRuleBoService();
439    }
440
441    public KrmsTypeRepositoryService getKrmsTypeRepositoryService() {
442        return KrmsRepositoryServiceLocator.getKrmsTypeRepositoryService();
443    }
444
445    public ActionTypeService getActionTypeService(String serviceName) {
446        return (ActionTypeService)KrmsRepositoryServiceLocator.getService(serviceName);
447    }
448
449    public AgendaAuthorizationService getAgendaAuthorizationService() {
450        return KrmsRepositoryServiceLocator.getAgendaAuthorizationService();
451    }
452
453}
454