View Javadoc
1   /**
2    * Copyright 2005-2014 The Kuali Foundation
3    *
4    * Licensed under the Educational Community License, Version 2.0 (the "License");
5    * you may not use this file except in compliance with the License.
6    * You may obtain a copy of the License at
7    *
8    * http://www.opensource.org/licenses/ecl2.php
9    *
10   * Unless required by applicable law or agreed to in writing, software
11   * distributed under the License is distributed on an "AS IS" BASIS,
12   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13   * See the License for the specific language governing permissions and
14   * limitations under the License.
15   */
16  package org.kuali.rice.krms.impl.ui;
17  
18  import org.apache.commons.collections.CollectionUtils;
19  import org.apache.commons.lang.StringUtils;
20  import org.apache.ojb.broker.metadata.ClassNotPersistenceCapableException;
21  import org.kuali.rice.core.api.data.DataType;
22  import org.kuali.rice.core.api.resourceloader.GlobalResourceLoader;
23  import org.kuali.rice.core.api.uif.RemotableAttributeField;
24  import org.kuali.rice.core.api.uif.RemotableTextInput;
25  import org.kuali.rice.core.api.util.tree.Node;
26  import org.kuali.rice.core.api.util.tree.Tree;
27  import org.kuali.rice.core.impl.cache.DistributedCacheManagerDecorator;
28  import org.kuali.rice.krad.data.PersistenceOption;
29  import org.kuali.rice.krad.maintenance.Maintainable;
30  import org.kuali.rice.krad.maintenance.MaintainableImpl;
31  import org.kuali.rice.krad.maintenance.MaintenanceDocument;
32  import org.kuali.rice.krad.uif.container.Container;
33  import org.kuali.rice.krad.uif.view.View;
34  import org.kuali.rice.krad.uif.view.ViewModel;
35  import org.kuali.rice.krad.util.KRADConstants;
36  import org.kuali.rice.krad.util.ObjectUtils;
37  import org.kuali.rice.krad.web.form.MaintenanceDocumentForm;
38  import org.kuali.rice.krms.api.KrmsConstants;
39  import org.kuali.rice.krms.api.repository.action.ActionDefinition;
40  import org.kuali.rice.krms.api.repository.agenda.AgendaDefinition;
41  import org.kuali.rice.krms.api.repository.agenda.AgendaItemDefinition;
42  import org.kuali.rice.krms.api.repository.agenda.AgendaTreeDefinition;
43  import org.kuali.rice.krms.api.repository.context.ContextDefinition;
44  import org.kuali.rice.krms.api.repository.function.FunctionDefinition;
45  import org.kuali.rice.krms.api.repository.operator.CustomOperator;
46  import org.kuali.rice.krms.api.repository.proposition.PropositionDefinition;
47  import org.kuali.rice.krms.api.repository.proposition.PropositionParameterType;
48  import org.kuali.rice.krms.api.repository.rule.RuleDefinition;
49  import org.kuali.rice.krms.api.repository.term.TermDefinition;
50  import org.kuali.rice.krms.api.repository.term.TermResolverDefinition;
51  import org.kuali.rice.krms.api.repository.term.TermSpecificationDefinition;
52  import org.kuali.rice.krms.api.repository.type.KrmsAttributeDefinition;
53  import org.kuali.rice.krms.api.repository.type.KrmsTypeDefinition;
54  import org.kuali.rice.krms.impl.repository.ActionAttributeBo;
55  import org.kuali.rice.krms.impl.repository.ActionBo;
56  import org.kuali.rice.krms.impl.repository.AgendaBo;
57  import org.kuali.rice.krms.impl.repository.AgendaItemBo;
58  import org.kuali.rice.krms.impl.repository.ContextBoService;
59  import org.kuali.rice.krms.impl.repository.KrmsAttributeDefinitionService;
60  import org.kuali.rice.krms.impl.repository.KrmsRepositoryServiceLocator;
61  import org.kuali.rice.krms.impl.repository.PropositionBo;
62  import org.kuali.rice.krms.impl.repository.PropositionParameterBo;
63  import org.kuali.rice.krms.impl.repository.RepositoryBoIncrementer;
64  import org.kuali.rice.krms.impl.repository.RuleAttributeBo;
65  import org.kuali.rice.krms.impl.repository.RuleBo;
66  import org.kuali.rice.krms.impl.repository.TermBo;
67  import org.kuali.rice.krms.impl.repository.TermParameterBo;
68  import org.kuali.rice.krms.impl.util.KrmsImplConstants;
69  import org.kuali.rice.krms.impl.util.KrmsRetriever;
70  import org.kuali.rice.krms.impl.util.KrmsServiceLocatorInternal;
71  
72  import java.util.ArrayList;
73  import java.util.Collections;
74  import java.util.Date;
75  import java.util.HashMap;
76  import java.util.List;
77  import java.util.Map;
78  
79  import static org.kuali.rice.krms.impl.repository.BusinessObjectServiceMigrationUtils.findSingleMatching;
80  
81  /**
82   * {@link Maintainable} for the {@link AgendaEditor}
83   *
84   * @author Kuali Rice Team (rice.collab@kuali.org)
85   */
86  public class AgendaEditorMaintainable extends MaintainableImpl {
87  
88      private static final long serialVersionUID = 1L;
89  
90      private static final org.apache.log4j.Logger LOG = org.apache.log4j.Logger.getLogger(
91              AgendaEditorMaintainable.class);
92  
93      public static final String NEW_AGENDA_EDITOR_DOCUMENT_TEXT = "New Agenda Editor Document";
94      private static final RepositoryBoIncrementer termIdIncrementer = new RepositoryBoIncrementer(TermBo.TERM_SEQ_NAME);
95      private static final RepositoryBoIncrementer termParameterIdIncrementer = new RepositoryBoIncrementer(TermParameterBo.TERM_PARM_SEQ_NAME);
96      private static final RepositoryBoIncrementer agendaItemIncrementer = new RepositoryBoIncrementer(AgendaBo.AGENDA_SEQ_NAME);
97  
98      private transient KrmsRetriever krmsRetriever = new KrmsRetriever();
99  
100     /**
101      * return the contextBoService
102      */
103     private ContextBoService getContextBoService() {
104         return KrmsRepositoryServiceLocator.getContextBoService();
105     }
106 
107     public List<RemotableAttributeField> retrieveAgendaCustomAttributes(View view, Object model, Container container) {
108         AgendaEditor agendaEditor = getAgendaEditor(model);
109         return krmsRetriever.retrieveAgendaCustomAttributes(agendaEditor);
110     }
111 
112     /**
113      * Retrieve a list of {@link RemotableAttributeField}s for the parameters (if any) required by the resolver for
114      * the selected term in the proposition that is under edit.
115      */
116     public List<RemotableAttributeField> retrieveTermParameters(View view, Object model, Container container) {
117 
118         List<RemotableAttributeField> results = new ArrayList<RemotableAttributeField>();
119 
120         AgendaEditor agendaEditor = getAgendaEditor(model);
121 
122         // Figure out which rule is being edited
123         RuleBo rule = agendaEditor.getAgendaItemLine().getRule();
124         if (null != rule) {
125 
126             // Figure out which proposition is being edited
127             Tree<RuleTreeNode, String> propositionTree = rule.getPropositionTree();
128             Node<RuleTreeNode, String> editedPropositionNode = findEditedProposition(propositionTree.getRootElement());
129 
130             if (editedPropositionNode != null) {
131                 PropositionBo propositionBo = editedPropositionNode.getData().getProposition();
132                 if (StringUtils.isEmpty(propositionBo.getCompoundOpCode()) && CollectionUtils.size(
133                         propositionBo.getParameters()) > 0) {
134                     // Get the term ID; if it is a new parameterized term, it will have a special prefix
135                     PropositionParameterBo param = propositionBo.getParameters().get(0);
136                     if (StringUtils.isNotBlank(param.getValue()) &&
137                             param.getValue().startsWith(KrmsImplConstants.PARAMETERIZED_TERM_PREFIX)) {
138                         String termSpecId = param.getValue().substring(
139                                 KrmsImplConstants.PARAMETERIZED_TERM_PREFIX.length());
140                         TermResolverDefinition simplestResolver = getSimplestTermResolver(termSpecId,
141                                 rule.getNamespace());
142 
143                         // Get the parameters and build RemotableAttributeFields
144                         if (simplestResolver != null) {
145                             List<String> parameterNames = new ArrayList<String>(simplestResolver.getParameterNames());
146                             Collections.sort(parameterNames); // make param order deterministic
147 
148                             for (String parameterName : parameterNames) {
149                                 // TODO: also allow for DD parameters if there are matching type attributes
150                                 RemotableTextInput.Builder controlBuilder = RemotableTextInput.Builder.create();
151                                 controlBuilder.setSize(64);
152 
153                                 RemotableAttributeField.Builder builder = RemotableAttributeField.Builder.create(
154                                         parameterName);
155 
156                                 builder.setRequired(true);
157                                 builder.setDataType(DataType.STRING);
158                                 builder.setControl(controlBuilder);
159                                 builder.setLongLabel(parameterName);
160                                 builder.setShortLabel(parameterName);
161                                 builder.setMinLength(Integer.valueOf(1));
162                                 builder.setMaxLength(Integer.valueOf(64));
163 
164                                 results.add(builder.build());
165                             }
166                         }
167                     }
168                 }
169             }
170         }
171         return results;
172     }
173 
174     /**
175      * finds the term resolver with the fewest parameters that resolves the given term specification
176      *
177      * @param termSpecId the id of the term specification
178      * @param namespace the  namespace of the term specification
179      * @return the simples {@link TermResolverDefinition} found, or null if none was found
180      */
181     // package access so that AgendaEditorController can use it too
182     static TermResolverDefinition getSimplestTermResolver(String termSpecId,
183             String namespace) {// Get the term resolver for the term spec
184 
185         List<TermResolverDefinition> resolvers =
186                 KrmsRepositoryServiceLocator.getTermBoService().findTermResolversByOutputId(termSpecId, namespace);
187 
188         TermResolverDefinition simplestResolver = null;
189 
190         for (TermResolverDefinition resolver : resolvers) {
191             if (simplestResolver == null || simplestResolver.getParameterNames().size() < resolver.getParameterNames()
192                     .size()) {
193                 simplestResolver = resolver;
194             }
195         }
196 
197         return simplestResolver;
198     }
199 
200     /**
201      * Find and return the node containing the proposition that is in currently in edit mode
202      *
203      * @param node the node to start searching from (typically the root)
204      * @return the node that is currently being edited, if any.  Otherwise, null.
205      */
206     private Node<RuleTreeNode, String> findEditedProposition(Node<RuleTreeNode, String> node) {
207         Node<RuleTreeNode, String> result = null;
208         if (node.getData() != null && node.getData().getProposition() != null && node.getData().getProposition()
209                 .getEditMode()) {
210             result = node;
211         } else {
212             for (Node<RuleTreeNode, String> child : node.getChildren()) {
213                 result = findEditedProposition(child);
214                 if (result != null) {
215                     break;
216                 }
217             }
218         }
219         return result;
220     }
221 
222     /**
223      * Get the AgendaEditor out of the MaintenanceDocumentForm's newMaintainableObject
224      *
225      * @param model the MaintenanceDocumentForm
226      * @return the AgendaEditor
227      */
228     private AgendaEditor getAgendaEditor(Object model) {
229         MaintenanceDocumentForm maintenanceForm = (MaintenanceDocumentForm) model;
230         return (AgendaEditor) maintenanceForm.getDocument().getNewMaintainableObject().getDataObject();
231     }
232 
233     public List<RemotableAttributeField> retrieveRuleActionCustomAttributes(View view, Object model,
234             Container container) {
235         AgendaEditor agendaEditor = getAgendaEditor((MaintenanceDocumentForm) model);
236         return krmsRetriever.retrieveRuleActionCustomAttributes(agendaEditor);
237     }
238 
239     /**
240      * This only supports a single action within a rule.
241      */
242     public List<RemotableAttributeField> retrieveRuleCustomAttributes(View view, Object model, Container container) {
243         AgendaEditor agendaEditor = getAgendaEditor((MaintenanceDocumentForm) model);
244         return krmsRetriever.retrieveRuleCustomAttributes(agendaEditor);
245     }
246 
247     @Override
248     public Object retrieveObjectForEditOrCopy(MaintenanceDocument document, Map<String, String> dataObjectKeys) {
249         Object dataObject = null;
250 
251         try {
252             // Since the dataObject is a wrapper class we need to build it and populate with the agenda bo.
253             AgendaEditor agendaEditor = new AgendaEditor();
254             AgendaBo agenda = findSingleMatching(getDataObjectService(),
255                     ((AgendaEditor) getDataObject()).getAgenda().getClass(), dataObjectKeys);
256 
257             // HACK: force lazy loaded items to be fetched
258             forceLoadLazyRelations(agenda);
259 
260             if (KRADConstants.MAINTENANCE_COPY_ACTION.equals(getMaintenanceAction())) {
261                 String dateTimeStamp = (new Date()).getTime() + "";
262                 String newAgendaName = AgendaItemBo.COPY_OF_TEXT + agenda.getName() + " " + dateTimeStamp;
263 
264                 AgendaBo copiedAgenda = agenda.copyAgenda(newAgendaName, dateTimeStamp);
265 
266                 document.getDocumentHeader().setDocumentDescription(NEW_AGENDA_EDITOR_DOCUMENT_TEXT);
267                 document.setFieldsClearedOnCopy(true);
268                 agendaEditor.setAgenda(copiedAgenda);
269             } else {
270                 // set custom attributes map in AgendaEditor
271                 //                agendaEditor.setCustomAttributesMap(agenda.getAttributes());
272                 agendaEditor.setAgenda(agenda);
273             }
274             agendaEditor.setCustomAttributesMap(agenda.getAttributes());
275 
276             // set extra fields on AgendaEditor
277             agendaEditor.setNamespace(agenda.getContext().getNamespace());
278             agendaEditor.setContextName(agenda.getContext().getName());
279 
280             dataObject = agendaEditor;
281         } catch (ClassNotPersistenceCapableException ex) {
282             if (!document.getOldMaintainableObject().isExternalBusinessObject()) {
283                 throw new RuntimeException("Data Object Class: "
284                         + getDataObjectClass()
285                         + " is not persistable and is not externalizable - configuration error");
286             }
287             // otherwise, let fall through
288         }
289 
290         return dataObject;
291     }
292 
293     private void forceLoadLazyRelations(AgendaBo agenda) {
294         for (AgendaItemBo item : agenda.getItems()) {
295             for (ActionBo action : item.getRule().getActions()) {
296                 if (CollectionUtils.isEmpty(action.getAttributeBos())) { continue; }
297 
298                 for (ActionAttributeBo actionAttribute : action.getAttributeBos()) {
299                     actionAttribute.getAttributeDefinition();
300                 }
301             }
302 
303             Tree propTree = item.getRule().refreshPropositionTree(true);
304             walkPropositionTree(item.getRule().getProposition());
305 
306             for (RuleAttributeBo ruleAttribute : item.getRule().getAttributeBos()) {
307                 ruleAttribute.getAttributeDefinition();
308             }
309         }
310     }
311 
312     private void walkPropositionTree(PropositionBo prop) {
313         if (prop == null) { return; }
314 
315         if (prop.getParameters() != null) for (PropositionParameterBo param : prop.getParameters()) {
316             param.getPropId();
317         }
318 
319         if (prop.getCompoundComponents() != null) for (PropositionBo childProp : prop.getCompoundComponents()) {
320             walkPropositionTree(childProp);
321         }
322     }
323 
324     /**
325      * {@inheritDoc}
326      */
327     @Override
328     public void processAfterNew(MaintenanceDocument document, Map<String, String[]> requestParameters) {
329         super.processAfterNew(document, requestParameters);
330         document.getDocumentHeader().setDocumentDescription(NEW_AGENDA_EDITOR_DOCUMENT_TEXT);
331     }
332 
333     @Override
334     public void processAfterEdit(MaintenanceDocument document, Map<String, String[]> requestParameters) {
335         super.processAfterEdit(document, requestParameters);
336         document.getDocumentHeader().setDocumentDescription("Modify Agenda Editor Document");
337     }
338 
339     @Override
340     public void processAfterCopy(MaintenanceDocument document,
341             Map<String, String[]> parameters) {
342         super.processAfterCopy(document, parameters);
343         AgendaBo agendaBo = ((AgendaEditor) document.getDocumentDataObject()).getAgenda();
344         agendaBo.setVersionNumber(null);
345 
346         for (AgendaItemBo agendaItem : agendaBo.getItems()) {
347             agendaItem.setVersionNumber(null);
348         }
349     }
350     @Override
351     public void prepareForSave() {
352         // set agenda attributes
353         AgendaEditor agendaEditor = (AgendaEditor) getDataObject();
354         agendaEditor.getAgenda().setAttributes(agendaEditor.getCustomAttributesMap());
355     }
356 
357     @Override
358     public void saveDataObject() {
359         AgendaBo agendaBo = ((AgendaEditor) getDataObject()).getAgenda();
360 
361         AgendaItemBo firstItem = null;
362 
363         // Find the first agenda item
364         for (AgendaItemBo agendaItem : agendaBo.getItems()) {
365             if (agendaBo.getFirstItemId().equals(agendaItem.getId())) {
366                 firstItem = agendaItem;
367             }
368         }
369 
370         // if new agenda persist without items.  This works around a chicken and egg problem
371         // with KRMS_AGENDA_T.INIT_AGENDA_ITM_ID and KRMS_AGENDA_ITM_T.AGENDA_ITM_ID both being non-nullable
372         List<AgendaItemBo> agendaItems = agendaBo.getItems();
373         List<AgendaItemBo> updatedItems = new ArrayList<AgendaItemBo>();
374         if (agendaBo.getId() == null || getDataObjectService().find(AgendaBo.class,agendaBo.getId()) == null) {
375             agendaBo.setItems(updatedItems);
376             agendaBo.setFirstItem(null);
377             agendaBo = getDataObjectService().save(agendaBo);
378             getDataObjectService().flush(AgendaBo.class);
379             String agendaBoId = agendaBo.getId();
380             agendaBo = getDataObjectService().find(AgendaBo.class,agendaBoId);
381             agendaBo.setItems(agendaItems);
382             agendaBo.setFirstItem(firstItem);
383         }
384 
385 
386         // handle saving new parameterized terms and processing custom operators
387         for (AgendaItemBo agendaItem : agendaBo.getItems()) {
388             PropositionBo propositionBo = agendaItem.getRule().getProposition();
389             if (propositionBo != null) {
390                 saveNewParameterizedTerms(propositionBo);
391                 processCustomOperators(propositionBo);
392             }
393         }
394 
395         if (agendaBo != null) {
396             flushCacheBeforeSave();
397             getDataObjectService().flush(AgendaBo.class);
398 
399             // Need to set the first item for persistence to cascade
400             agendaBo.setFirstItem(firstItem);
401             getDataObjectService().save(agendaBo);
402 
403             // delete orphaned propositions -- this may not be as comprehensive as it should
404             for (String deletedPropId : ((AgendaEditor) getDataObject()).getDeletedPropositionIds()) {
405                 PropositionBo toDelete = getDataObjectService().find(PropositionBo.class, deletedPropId);
406 
407                 deletePropositionFromTree(toDelete);
408             }
409         } else {
410             throw new RuntimeException("Cannot save null " + AgendaBo.class.getName() + " with business object service");
411         }
412     }
413 
414     /**
415      * removes all parent-child relationships from this proposition, and deletes it.
416      *
417      * <p><B>NOTE:</B> This method assumes that the proposition exists in the database.</p>
418      *
419      * @param toDelete the proposition to delete
420      */
421     private void deletePropositionFromTree(PropositionBo toDelete) {
422         if (toDelete != null) {
423             // clear out children to nuke join table rows
424             if (!CollectionUtils.isEmpty(toDelete.getCompoundComponents())) {
425                 toDelete.getCompoundComponents().clear();
426             }
427 
428             // save it first to delete the join table rows
429             getDataObjectService().save(toDelete);
430             getDataObjectService().delete(toDelete);
431         }
432     }
433 
434     private void addItemsToListForDeletion(List<AgendaItemBo> deletionOrder, AgendaItemBo agendaItemBo){
435         if (!deletionOrder.contains(agendaItemBo) && ObjectUtils.isNotNull(agendaItemBo)) {
436             deletionOrder.add(agendaItemBo);
437         }
438         if (ObjectUtils.isNotNull(agendaItemBo)) {
439             if (StringUtils.isNotBlank(agendaItemBo.getWhenTrueId()) &&
440                 !deletionOrder.contains(agendaItemBo.getWhenTrue())) {
441                     deletionOrder.add(agendaItemBo.getWhenTrue());
442                     addItemsToListForDeletion (deletionOrder, agendaItemBo.getWhenTrue());
443             }
444             if (StringUtils.isNotBlank(agendaItemBo.getWhenFalseId()) &&
445                 !deletionOrder.contains(agendaItemBo.getWhenFalse())) {
446                     deletionOrder.add(agendaItemBo.getWhenFalse());
447                     addItemsToListForDeletion (deletionOrder, agendaItemBo.getWhenFalse());
448             }
449             if (StringUtils.isNotBlank(agendaItemBo.getAlwaysId()) &&
450                 !deletionOrder.contains(agendaItemBo.getAlways())) {
451                     deletionOrder.add(agendaItemBo.getAlways());
452                     addItemsToListForDeletion (deletionOrder,agendaItemBo.getAlways());
453             }
454         }
455     }
456 
457     private void flushCacheBeforeSave(){
458         //flush krms caches
459         DistributedCacheManagerDecorator distributedCacheManagerDecorator =
460                 GlobalResourceLoader.getService(KrmsConstants.KRMS_DISTRIBUTED_CACHE);
461 
462         distributedCacheManagerDecorator.getCache(ActionDefinition.Cache.NAME).clear();
463         distributedCacheManagerDecorator.getCache(AgendaItemDefinition.Cache.NAME).clear();
464         distributedCacheManagerDecorator.getCache(AgendaTreeDefinition.Cache.NAME).clear();
465         distributedCacheManagerDecorator.getCache(AgendaDefinition.Cache.NAME).clear();
466         distributedCacheManagerDecorator.getCache(ContextDefinition.Cache.NAME).clear();
467         distributedCacheManagerDecorator.getCache(KrmsAttributeDefinition.Cache.NAME).clear();
468         distributedCacheManagerDecorator.getCache(KrmsTypeDefinition.Cache.NAME).clear();
469         distributedCacheManagerDecorator.getCache(RuleDefinition.Cache.NAME).clear();
470         distributedCacheManagerDecorator.getCache(PropositionDefinition.Cache.NAME).clear();
471         distributedCacheManagerDecorator.getCache(RuleDefinition.Cache.NAME).clear();
472         distributedCacheManagerDecorator.getCache(TermDefinition.Cache.NAME).clear();
473         distributedCacheManagerDecorator.getCache(TermResolverDefinition.Cache.NAME).clear();
474         distributedCacheManagerDecorator.getCache(TermSpecificationDefinition.Cache.NAME).clear();
475     }
476 
477     /**
478      * walk the proposition tree and save any new parameterized terms that are contained therein
479      *
480      * @param propositionBo the root proposition from which to search
481      */
482     private void saveNewParameterizedTerms(PropositionBo propositionBo) {
483         if (StringUtils.isBlank(propositionBo.getCompoundOpCode())) {
484             // it is a simple proposition
485             if (!propositionBo.getParameters().isEmpty() && propositionBo.getParameters().get(0).getValue().startsWith(
486                     KrmsImplConstants.PARAMETERIZED_TERM_PREFIX)) {
487                 String termId = propositionBo.getParameters().get(0).getValue();
488                 String termSpecId = termId.substring(KrmsImplConstants.PARAMETERIZED_TERM_PREFIX.length());
489                 // create new term
490                 TermBo newTerm = new TermBo();
491                 newTerm.setDescription(propositionBo.getNewTermDescription());
492                 newTerm.setSpecificationId(termSpecId);
493                 newTerm.setId(termIdIncrementer.getNewId());
494 
495                 List<TermParameterBo> params = new ArrayList<TermParameterBo>();
496                 for (Map.Entry<String, String> entry : propositionBo.getTermParameters().entrySet()) {
497                     TermParameterBo param = new TermParameterBo();
498                     param.setTerm(newTerm);
499                     param.setName(entry.getKey());
500                     param.setValue(entry.getValue());
501                     param.setId(termParameterIdIncrementer.getNewId());
502 
503                     params.add(param);
504                 }
505 
506                 newTerm.setParameters(params);
507 
508                 getLegacyDataAdapter().linkAndSave(newTerm);
509                 propositionBo.getParameters().get(0).setValue(newTerm.getId());
510             }
511         } else {
512             // recurse
513             for (PropositionBo childProp : propositionBo.getCompoundComponents()) {
514                 saveNewParameterizedTerms(childProp);
515             }
516         }
517     }
518 
519     /**
520      * walk the proposition tree and process any custom operators found, converting them to custom function invocations.
521      *
522      * @param propositionBo the root proposition from which to search and convert
523      */
524     private void processCustomOperators(PropositionBo propositionBo) {
525         if (StringUtils.isBlank(propositionBo.getCompoundOpCode())) {
526             // if it is a simple proposition with a custom operator
527             if (!propositionBo.getParameters().isEmpty() && propositionBo.getParameters().get(2).getValue().startsWith(
528                     KrmsImplConstants.CUSTOM_OPERATOR_PREFIX)) {
529                 PropositionParameterBo operatorParam = propositionBo.getParameters().get(2);
530 
531                 CustomOperator customOperator =
532                         KrmsServiceLocatorInternal.getCustomOperatorUiTranslator().getCustomOperator(operatorParam.getValue());
533 
534                 FunctionDefinition operatorFunctionDefinition = customOperator.getOperatorFunctionDefinition();
535 
536                 operatorParam.setParameterType(PropositionParameterType.FUNCTION.getCode());
537                 operatorParam.setValue(operatorFunctionDefinition.getId());
538             }
539         } else {
540             // recurse
541             for (PropositionBo childProp : propositionBo.getCompoundComponents()) {
542                 processCustomOperators(childProp);
543             }
544         }
545     }
546 
547     /**
548      * Build a map from attribute name to attribute definition from all the defined attribute definitions for the
549      * specified agenda type
550      *
551      * @param agendaTypeId
552      * @return
553      */
554     private Map<String, KrmsAttributeDefinition> buildAttributeDefinitionMap(String agendaTypeId) {
555         KrmsAttributeDefinitionService attributeDefinitionService =
556                 KrmsRepositoryServiceLocator.getKrmsAttributeDefinitionService();
557 
558         // build a map from attribute name to definition
559         Map<String, KrmsAttributeDefinition> attributeDefinitionMap = new HashMap<String, KrmsAttributeDefinition>();
560 
561         List<KrmsAttributeDefinition> attributeDefinitions = attributeDefinitionService.findAttributeDefinitionsByType(
562                 agendaTypeId);
563 
564         for (KrmsAttributeDefinition attributeDefinition : attributeDefinitions) {
565             attributeDefinitionMap.put(attributeDefinition.getName(), attributeDefinition);
566         }
567         return attributeDefinitionMap;
568     }
569 
570     @Override
571     public boolean isOldDataObjectInDocument() {
572         boolean isOldDataObjectInExistence = true;
573 
574         if (getDataObject() == null) {
575             isOldDataObjectInExistence = false;
576         } else {
577             // dataObject contains a non persistable wrapper - use agenda from the wrapper object instead
578             Map<String, ?> keyFieldValues = getLegacyDataAdapter().getPrimaryKeyFieldValues(
579                     ((AgendaEditor) getDataObject()).getAgenda());
580             for (Object keyValue : keyFieldValues.values()) {
581                 if (keyValue == null) {
582                     isOldDataObjectInExistence = false;
583                 } else if ((keyValue instanceof String) && StringUtils.isBlank((String) keyValue)) {
584                     isOldDataObjectInExistence = false;
585                 }
586 
587                 if (!isOldDataObjectInExistence) {
588                     break;
589                 }
590             }
591         }
592 
593         return isOldDataObjectInExistence;
594     }
595 
596     // Since the dataObject is a wrapper class we need to return the agendaBo instead.
597     @Override
598     public Class getDataObjectClass() {
599         return AgendaBo.class;
600     }
601 
602     @Override
603     public boolean isLockable() {
604         return true;
605     }
606 
607     @Override
608     public void processBeforeAddLine(ViewModel model, Object addLine, String collectionId, String collectionPath) {
609         AgendaEditor agendaEditor = getAgendaEditor(model);
610         if (addLine instanceof ActionBo) {
611             ((ActionBo) addLine).setNamespace(agendaEditor.getAgendaItemLine().getRule().getNamespace());
612         }
613 
614         super.processBeforeAddLine(model, addLine, collectionId, collectionPath);
615     }
616 }