001/**
002 * Copyright 2005-2014 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.service.KRADServiceLocatorWeb;
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.ActionBo;
037import org.kuali.rice.krms.impl.repository.AgendaBo;
038import org.kuali.rice.krms.impl.repository.AgendaBoService;
039import org.kuali.rice.krms.impl.repository.AgendaItemBo;
040import org.kuali.rice.krms.impl.repository.ContextBoService;
041import org.kuali.rice.krms.impl.repository.KrmsRepositoryServiceLocator;
042import org.kuali.rice.krms.impl.repository.RuleBo;
043import org.kuali.rice.krms.impl.repository.RuleBoService;
044import org.kuali.rice.krms.impl.ui.AgendaEditor;
045import org.kuali.rice.krms.impl.util.KRMSPropertyConstants;
046
047import java.util.List;
048import java.util.Map;
049
050import static org.kuali.rice.krms.impl.repository.BusinessObjectServiceMigrationUtils.findSingleMatching;
051
052/**
053 * This class contains the rules for the AgendaEditor.
054 */
055public 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 (!KRADServiceLocatorWeb.getLegacyDataAdapter().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 = KRADServiceLocatorWeb.getLegacyDataAdapter().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 = findSingleMatching(getDataObjectService(),
105                        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        if (StringUtils.isNotBlank(agendaEditor.getNamespace()) &&
172                getContextBoService().getContextByNameAndNamespace(agendaEditor.getContextName(), agendaEditor.getNamespace()) != null) {
173            return true;
174        } else {
175            this.putFieldError(KRMSPropertyConstants.Context.NAMESPACE, "error.context.invalidNamespace");
176            return false;
177        }
178    }
179
180    private boolean validAgendaTypeAndAttributes( AgendaEditor oldAgendaEditor, AgendaEditor newAgendaEditor) {
181        if (validAgendaType(newAgendaEditor.getAgenda().getTypeId(), newAgendaEditor.getAgenda().getContextId())) {
182            return validAgendaAttributes(oldAgendaEditor, newAgendaEditor);
183        } else {
184            return false;
185        }
186    }
187    private boolean validAgendaType(String typeId, String contextId) {
188        boolean isValid = true;
189
190        if (!StringUtils.isBlank(typeId) && !StringUtils.isBlank(contextId)) {
191            if (getKrmsTypeRepositoryService().getAgendaTypeByAgendaTypeIdAndContextId(typeId, contextId) != null) {
192                return true;
193            } else {
194                this.putFieldError(KRMSPropertyConstants.Agenda.TYPE, "error.agenda.invalidType");
195                return false;
196            }
197        }
198
199        return isValid;
200    }
201
202    private boolean validAgendaAttributes(AgendaEditor oldAgendaEditor, AgendaEditor newAgendaEditor) {
203        boolean isValid = true;
204
205        String typeId = newAgendaEditor.getAgenda().getTypeId();
206
207        if (!StringUtils.isEmpty(typeId)) {
208            KrmsTypeDefinition typeDefinition = getKrmsTypeRepositoryService().getTypeById(typeId);
209
210            if (typeDefinition == null) {
211                throw new IllegalStateException("agenda typeId must match the id of a valid krms type");
212            } else if (StringUtils.isBlank(typeDefinition.getServiceName())) {
213                throw new IllegalStateException("agenda type definition must have a non-blank service name");
214            } else {
215                AgendaTypeService agendaTypeService =
216                        (AgendaTypeService)KrmsRepositoryServiceLocator.getService(typeDefinition.getServiceName());
217
218                if (agendaTypeService == null) {
219                    throw new IllegalStateException("typeDefinition must have a valid serviceName");
220                } else {
221
222                    List<RemotableAttributeError> errors;
223                    if (oldAgendaEditor == null) {
224                        errors = agendaTypeService.validateAttributes(typeId, newAgendaEditor.getCustomAttributesMap());
225                    } else {
226                        errors = agendaTypeService.validateAttributesAgainstExisting(typeId, newAgendaEditor.getCustomAttributesMap(), oldAgendaEditor.getCustomAttributesMap());
227                    }
228
229                    if (!CollectionUtils.isEmpty(errors)) {
230                        isValid = false;
231                        for (RemotableAttributeError error : errors) {
232                            for (String errorStr : error.getErrors()) {
233                                this.putFieldError(
234                                        KRMSPropertyConstants.AgendaEditor.CUSTOM_ATTRIBUTES_MAP +
235                                                "['" + error.getAttributeName() + "']",
236                                        errorStr
237                                );
238                            }
239                        }
240                    }
241                }
242            }
243        }
244        return isValid;
245    }
246
247    /**
248     * Check if an agenda with that name exists already in the context.
249     * @param agendaEditor
250     * @return true if agenda name is unique, false otherwise
251     */
252    public boolean validAgendaName(AgendaEditor agendaEditor) {
253        try {
254            AgendaDefinition agendaFromDataBase = getAgendaBoService().getAgendaByNameAndContextId(
255                    agendaEditor.getAgenda().getName(), agendaEditor.getAgenda().getContextId());
256            if ((agendaFromDataBase != null) && !StringUtils.equals(agendaFromDataBase.getId(), agendaEditor.getAgenda().getId())) {
257                this.putFieldError(KRMSPropertyConstants.Agenda.NAME, "error.agenda.duplicateName");
258                return false;
259            }
260        }
261        catch (IllegalArgumentException e) {
262            this.putFieldError(KRMSPropertyConstants.Agenda.NAME, "error.agenda.invalidName");
263            return false;
264        }
265        return true;
266    }
267
268    /**
269     * Check if a agenda item is valid.
270     *
271     * @param document, the Agenda document of the added/edited agenda item
272     * @return true if agenda item is valid, false otherwise
273     */
274    public boolean processAgendaItemBusinessRules(MaintenanceDocument document) {
275        boolean isValid = true;
276
277        AgendaEditor newAgendaEditor = (AgendaEditor) document.getNewMaintainableObject().getDataObject();
278        AgendaEditor oldAgendaEditor = (AgendaEditor) document.getOldMaintainableObject().getDataObject();
279        RuleBo rule = newAgendaEditor.getAgendaItemLine().getRule();
280        isValid &= validateRuleName(rule, newAgendaEditor.getAgenda());
281        isValid &= validRuleType(rule.getTypeId(), newAgendaEditor.getAgenda().getContextId());
282        isValid &= validateRuleAction(oldAgendaEditor, newAgendaEditor);
283
284        return isValid;
285    }
286
287    /**
288     * Check if a rule with that name exists already in the namespace.
289     * @param rule
290     * @parm agenda
291     * @return true if rule name is unique, false otherwise
292     */
293    private boolean validateRuleName(RuleBo rule, AgendaBo agenda) {
294        if (StringUtils.isBlank(rule.getName())) {
295            this.putFieldError(KRMSPropertyConstants.Rule.NAME, "error.rule.invalidName");
296            return false;
297        }
298        // check current bo for rules (including ones that aren't persisted to the database)
299        for (AgendaItemBo agendaItem : agenda.getItems()) {
300            if (!StringUtils.equals(agendaItem.getRule().getId(), rule.getId()) && StringUtils.equals(agendaItem.getRule().getName(), rule.getName())
301                    && StringUtils.equals(agendaItem.getRule().getNamespace(), rule.getNamespace())) {
302                this.putFieldError(KRMSPropertyConstants.Rule.NAME, "error.rule.duplicateName");
303                return false;
304            }
305        }
306
307        // check database for rules used with other agendas - the namespace might not yet be specified on new agendas.
308        if (StringUtils.isNotBlank(rule.getNamespace())) {
309            RuleDefinition ruleFromDatabase = getRuleBoService().getRuleByNameAndNamespace(rule.getName(), rule.getNamespace());
310            try {
311                if ((ruleFromDatabase != null) && !StringUtils.equals(ruleFromDatabase.getId(), rule.getId())) {
312                    this.putFieldError(KRMSPropertyConstants.Rule.NAME, "error.rule.duplicateName");
313                    return false;
314                }
315            }
316            catch (IllegalArgumentException e) {
317                this.putFieldError(KRMSPropertyConstants.Rule.NAME, "error.rule.invalidName");
318                return false;
319            }
320        }
321        return true;
322    }
323
324    /**
325     * Check that the rule type is valid when specified.
326     * @param ruleTypeId, the type id
327     * @param contextId, the contextId the action needs to belong to.
328     * @return true if valid, false otherwise.
329     */
330    private boolean validRuleType(String ruleTypeId, String contextId) {
331        if (StringUtils.isBlank(ruleTypeId)) {
332            return true;
333        }
334
335        if (getKrmsTypeRepositoryService().getRuleTypeByRuleTypeIdAndContextId(ruleTypeId, contextId) != null) {
336            return true;
337        } else {
338            this.putFieldError(KRMSPropertyConstants.Rule.TYPE, "error.rule.invalidType");
339            return false;
340        }
341    }
342
343    private boolean validateRuleAction(AgendaEditor oldAgendaEditor, AgendaEditor newAgendaEditor) {
344        boolean isValid = true;
345        ActionBo newActionBo = newAgendaEditor.getAgendaItemLineRuleAction();
346
347        isValid &= validRuleActionType(newActionBo.getTypeId(), newAgendaEditor.getAgenda().getContextId());
348        if (isValid && StringUtils.isNotBlank(newActionBo.getTypeId())) {
349            isValid &= validRuleActionName(newActionBo.getName());
350            isValid &= validRuleActionAttributes(oldAgendaEditor, newAgendaEditor);
351        }
352        return isValid;
353    }
354
355    /**
356     * Check that the rule action type is valid when specified.
357     * @param typeId, the action type id
358     * @parm contextId, the contextId the action needs to belong to.
359     * @return true if valid, false otherwise.
360     */
361    private boolean validRuleActionType(String typeId, String contextId) {
362        if (StringUtils.isBlank(typeId)) {
363            return true;
364        }
365
366        if (getKrmsTypeRepositoryService().getActionTypeByActionTypeIdAndContextId(typeId, contextId) != null) {
367            return true;
368        } else {
369            this.putFieldError(KRMSPropertyConstants.Action.TYPE, "error.action.invalidType");
370            return false;
371        }
372    }
373
374    /**
375     * Check that a action name is specified.
376     */
377    private boolean validRuleActionName(String name) {
378        if (StringUtils.isNotBlank(name)) {
379            return true;
380        } else {
381            this.putFieldError(KRMSPropertyConstants.Action.NAME, "error.action.missingName");
382            return false;
383        }
384    }
385
386    private boolean validRuleActionAttributes(AgendaEditor oldAgendaEditor, AgendaEditor newAgendaEditor) {
387        boolean isValid = true;
388
389        String typeId = newAgendaEditor.getAgendaItemLineRuleAction().getTypeId();
390
391        if (!StringUtils.isBlank(typeId)) {
392            KrmsTypeDefinition typeDefinition = getKrmsTypeRepositoryService().getTypeById(typeId);
393
394            if (typeDefinition == null) {
395                throw new IllegalStateException("rule action typeId must match the id of a valid krms type");
396            } else if (StringUtils.isBlank(typeDefinition.getServiceName())) {
397                throw new IllegalStateException("rule action type definition must have a non-blank service name");
398            } else {
399                ActionTypeService actionTypeService = getActionTypeService(typeDefinition.getServiceName());
400
401                if (actionTypeService == null) {
402                    throw new IllegalStateException("typeDefinition must have a valid serviceName");
403                } else {
404
405                    List<RemotableAttributeError> errors;
406                    if (oldAgendaEditor == null) {
407                        errors = actionTypeService.validateAttributes(typeId,
408                                newAgendaEditor.getCustomRuleActionAttributesMap());
409                    } else {
410                        errors = actionTypeService.validateAttributesAgainstExisting(typeId,
411                                newAgendaEditor.getCustomRuleActionAttributesMap(), oldAgendaEditor.getCustomRuleActionAttributesMap());
412                    }
413
414                    if (!CollectionUtils.isEmpty(errors)) {
415                        isValid = false;
416                        for (RemotableAttributeError error : errors) {
417                            for (String errorStr : error.getErrors()) {
418                                this.putFieldError(
419                                        KRMSPropertyConstants.AgendaEditor.CUSTOM_RULE_ACTION_ATTRIBUTES_MAP +
420                                                "['" + error.getAttributeName() + "']",
421                                        errorStr
422                                );
423                            }
424                        }
425                    }
426                }
427            }
428        }
429        return isValid;
430    }
431
432    public ContextBoService getContextBoService() {
433        return KrmsRepositoryServiceLocator.getContextBoService();
434    }
435
436    public AgendaBoService getAgendaBoService() {
437        return KrmsRepositoryServiceLocator.getAgendaBoService();
438    }
439
440    public RuleBoService getRuleBoService() {
441        return KrmsRepositoryServiceLocator.getRuleBoService();
442    }
443
444    public KrmsTypeRepositoryService getKrmsTypeRepositoryService() {
445        return KrmsRepositoryServiceLocator.getKrmsTypeRepositoryService();
446    }
447
448    public ActionTypeService getActionTypeService(String serviceName) {
449        return (ActionTypeService)KrmsRepositoryServiceLocator.getService(serviceName);
450    }
451
452    public AgendaAuthorizationService getAgendaAuthorizationService() {
453        return KrmsRepositoryServiceLocator.getAgendaAuthorizationService();
454    }
455}
456