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