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