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.ui;
017    
018    import org.apache.commons.collections.CollectionUtils;
019    import org.apache.commons.lang.StringUtils;
020    import org.apache.ojb.broker.metadata.ClassNotPersistenceCapableException;
021    import org.kuali.rice.core.api.uif.DataType;
022    import org.kuali.rice.core.api.uif.RemotableAttributeField;
023    import org.kuali.rice.core.api.uif.RemotableTextInput;
024    import org.kuali.rice.core.api.util.tree.Node;
025    import org.kuali.rice.core.api.util.tree.Tree;
026    import org.kuali.rice.krad.bo.PersistableBusinessObject;
027    import org.kuali.rice.krad.maintenance.MaintenanceDocument;
028    import org.kuali.rice.krad.maintenance.Maintainable;
029    import org.kuali.rice.krad.maintenance.MaintainableImpl;
030    import org.kuali.rice.krad.service.BusinessObjectService;
031    import org.kuali.rice.krad.service.KRADServiceLocator;
032    import org.kuali.rice.krad.service.SequenceAccessorService;
033    import org.kuali.rice.krad.uif.container.CollectionGroup;
034    import org.kuali.rice.krad.uif.container.Container;
035    import org.kuali.rice.krad.uif.view.View;
036    import org.kuali.rice.krad.util.KRADConstants;
037    import org.kuali.rice.krad.web.form.MaintenanceDocumentForm;
038    import org.kuali.rice.krms.api.repository.term.TermResolverDefinition;
039    import org.kuali.rice.krms.api.repository.type.KrmsAttributeDefinition;
040    import org.kuali.rice.krms.impl.repository.ActionBo;
041    import org.kuali.rice.krms.impl.repository.AgendaBo;
042    import org.kuali.rice.krms.impl.repository.AgendaItemBo;
043    import org.kuali.rice.krms.impl.repository.ContextBoService;
044    import org.kuali.rice.krms.impl.repository.KrmsAttributeDefinitionService;
045    import org.kuali.rice.krms.impl.repository.KrmsRepositoryServiceLocator;
046    import org.kuali.rice.krms.impl.repository.PropositionBo;
047    import org.kuali.rice.krms.impl.repository.PropositionParameterBo;
048    import org.kuali.rice.krms.impl.repository.RuleBo;
049    import org.kuali.rice.krms.impl.repository.TermBo;
050    import org.kuali.rice.krms.impl.repository.TermParameterBo;
051    import org.kuali.rice.krms.impl.util.KrmsImplConstants;
052    import org.kuali.rice.krms.impl.util.KrmsRetriever;
053    
054    import java.util.ArrayList;
055    import java.util.Collections;
056    import java.util.Date;
057    import java.util.HashMap;
058    import java.util.List;
059    import java.util.Map;
060    
061    /**
062     * {@link Maintainable} for the {@link AgendaEditor}
063     *
064     * @author Kuali Rice Team (rice.collab@kuali.org)
065     *
066     */
067    public class AgendaEditorMaintainable extends MaintainableImpl {
068    
069        private static final long serialVersionUID = 1L;
070    
071        private static final org.apache.log4j.Logger LOG = org.apache.log4j.Logger.getLogger(AgendaEditorMaintainable.class);
072    
073        public static final String NEW_AGENDA_EDITOR_DOCUMENT_TEXT = "New Agenda Editor Document";
074    
075        private transient SequenceAccessorService sequenceAccessorService;
076    
077        private transient KrmsRetriever krmsRetriever = new KrmsRetriever();
078    
079        /**
080         * @return the boService
081         */
082        public BusinessObjectService getBoService() {
083            return KRADServiceLocator.getBusinessObjectService();
084        }
085    
086        /**
087         * return the contextBoService
088         */
089        private ContextBoService getContextBoService() {
090            return KrmsRepositoryServiceLocator.getContextBoService();
091        }
092    
093        public List<RemotableAttributeField> retrieveAgendaCustomAttributes(View view, Object model, Container container) {
094            AgendaEditor agendaEditor = getAgendaEditor(model);
095            return krmsRetriever.retrieveAgendaCustomAttributes(agendaEditor);
096        }
097    
098        /**
099         * Retrieve a list of {@link RemotableAttributeField}s for the parameters (if any) required by the resolver for
100         * the selected term in the proposition that is under edit.
101         */
102        public List<RemotableAttributeField> retrieveTermParameters(View view, Object model, Container container) {
103    
104            List<RemotableAttributeField> results = new ArrayList<RemotableAttributeField>();
105    
106            AgendaEditor agendaEditor = getAgendaEditor(model);
107    
108            // Figure out which rule is being edited
109            RuleBo rule = agendaEditor.getAgendaItemLine().getRule();
110            // Figure out which proposition is being edited
111            Tree<RuleTreeNode, String> propositionTree = rule.getPropositionTree();
112            Node<RuleTreeNode, String> editedPropositionNode = findEditedProposition(propositionTree.getRootElement());
113    
114            if (editedPropositionNode != null) {
115                PropositionBo propositionBo = editedPropositionNode.getData().getProposition();
116                if (StringUtils.isEmpty(propositionBo.getCompoundOpCode()) && CollectionUtils.size(propositionBo.getParameters()) > 0) {
117                    // Get the term ID; if it is a new parameterized term, it will have a special prefix
118                    PropositionParameterBo param = propositionBo.getParameters().get(0);
119                    if (param.getValue().startsWith(KrmsImplConstants.PARAMETERIZED_TERM_PREFIX)) {
120                        String termSpecId = param.getValue().substring(KrmsImplConstants.PARAMETERIZED_TERM_PREFIX.length());
121                        TermResolverDefinition simplestResolver = getSimplestTermResolver(termSpecId, rule.getNamespace());
122    
123                        // Get the parameters and build RemotableAttributeFields
124                        if (simplestResolver != null) {
125                            List<String> parameterNames = new ArrayList<String>(simplestResolver.getParameterNames());
126                            Collections.sort(parameterNames); // make param order deterministic
127    
128                            for (String parameterName : parameterNames) {
129                                // TODO: also allow for DD parameters if there are matching type attributes
130                                RemotableTextInput.Builder controlBuilder = RemotableTextInput.Builder.create();
131                                controlBuilder.setSize(64);
132    
133                                RemotableAttributeField.Builder builder = RemotableAttributeField.Builder.create(parameterName);
134    
135                                builder.setRequired(true);
136                                builder.setDataType(DataType.STRING);
137                                builder.setControl(controlBuilder);
138                                builder.setLongLabel(parameterName);
139                                builder.setShortLabel(parameterName);
140                                builder.setMinLength(Integer.valueOf(1));
141                                builder.setMaxLength(Integer.valueOf(64));
142    
143                                results.add(builder.build());
144                            }
145                        }
146                    }
147                }
148            }
149    
150            return results;
151        }
152    
153        /**
154         * finds the term resolver with the fewest parameters that resolves the given term specification
155         * @param termSpecId the id of the term specification
156         * @param namespace the  namespace of the term specification
157         * @return the simples {@link TermResolverDefinition} found, or null if none was found
158         */
159        // package access so that AgendaEditorController can use it too
160        static TermResolverDefinition getSimplestTermResolver(String termSpecId,
161                String namespace) {// Get the term resolver for the term spec
162    
163            List<TermResolverDefinition> resolvers =
164                    KrmsRepositoryServiceLocator.getTermBoService().findTermResolversByOutputId(
165                            termSpecId, namespace);
166    
167            TermResolverDefinition simplestResolver = null;
168    
169            for (TermResolverDefinition resolver : resolvers) {
170                if (simplestResolver == null ||
171                        simplestResolver.getParameterNames().size() < resolver.getParameterNames().size()) {
172                    simplestResolver = resolver;
173                }
174            }
175    
176            return simplestResolver;
177        }
178    
179        /**
180         * Find and return the node containing the proposition that is in currently in edit mode
181         * @param node the node to start searching from (typically the root)
182         * @return the node that is currently being edited, if any.  Otherwise, null.
183         */
184        private Node<RuleTreeNode, String> findEditedProposition(Node<RuleTreeNode, String> node) {
185            Node<RuleTreeNode, String> result = null;
186            if (node.getData() != null && node.getData().getProposition() != null &&
187                    node.getData().getProposition().getEditMode()) {
188                result = node;
189            } else {
190                for (Node<RuleTreeNode, String> child : node.getChildren()) {
191                    result = findEditedProposition(child);
192                    if (result != null) break;
193                }
194            }
195            return result;
196        }
197    
198        /**
199         * Get the AgendaEditor out of the MaintenanceDocumentForm's newMaintainableObject
200         * @param model the MaintenanceDocumentForm
201         * @return the AgendaEditor
202         */
203        private AgendaEditor getAgendaEditor(Object model) {
204            MaintenanceDocumentForm maintenanceForm = (MaintenanceDocumentForm)model;
205            return (AgendaEditor)maintenanceForm.getDocument().getNewMaintainableObject().getDataObject();
206        }
207    
208        public List<RemotableAttributeField> retrieveRuleActionCustomAttributes(View view, Object model, Container container) {
209            AgendaEditor agendaEditor = getAgendaEditor((MaintenanceDocumentForm) model);
210            return krmsRetriever.retrieveRuleActionCustomAttributes(agendaEditor);
211        }
212    
213        /**
214         *  This only supports a single action within a rule.
215         */
216        public List<RemotableAttributeField> retrieveRuleCustomAttributes(View view, Object model, Container container) {
217            AgendaEditor agendaEditor = getAgendaEditor((MaintenanceDocumentForm) model);
218            return krmsRetriever.retrieveRuleCustomAttributes(agendaEditor);
219        }
220    
221        @Override
222        public Object retrieveObjectForEditOrCopy(MaintenanceDocument document, Map<String, String> dataObjectKeys) {
223            Object dataObject = null;
224    
225            try {
226                // Since the dataObject is a wrapper class we need to build it and populate with the agenda bo.
227                AgendaEditor agendaEditor = new AgendaEditor();
228                AgendaBo agenda = getLookupService().findObjectBySearch(((AgendaEditor) getDataObject()).getAgenda().getClass(), dataObjectKeys);
229                if (KRADConstants.MAINTENANCE_COPY_ACTION.equals(getMaintenanceAction())) {
230                    String dateTimeStamp = (new Date()).getTime() + "";
231                    String newAgendaName = AgendaItemBo.COPY_OF_TEXT + agenda.getName() + " " + dateTimeStamp;
232    
233                    AgendaBo copiedAgenda = agenda.copyAgenda(newAgendaName, dateTimeStamp);
234    
235                    document.getDocumentHeader().setDocumentDescription(NEW_AGENDA_EDITOR_DOCUMENT_TEXT);
236                    document.setFieldsClearedOnCopy(true);
237                    agendaEditor.setAgenda(copiedAgenda);
238                } else {
239                    // set custom attributes map in AgendaEditor
240                    //                agendaEditor.setCustomAttributesMap(agenda.getAttributes());
241                    agendaEditor.setAgenda(agenda);
242                }
243                agendaEditor.setCustomAttributesMap(agenda.getAttributes());
244    
245    
246                // set extra fields on AgendaEditor
247                agendaEditor.setNamespace(agenda.getContext().getNamespace());
248                agendaEditor.setContextName(agenda.getContext().getName());
249    
250                dataObject = agendaEditor;
251            } catch (ClassNotPersistenceCapableException ex) {
252                if (!document.getOldMaintainableObject().isExternalBusinessObject()) {
253                    throw new RuntimeException("Data Object Class: " + getDataObjectClass() +
254                            " is not persistable and is not externalizable - configuration error");
255                }
256                // otherwise, let fall through
257            }
258    
259            return dataObject;
260        }
261    
262        /**
263         *  Returns the sequenceAssessorService
264         * @return {@link SequenceAccessorService}
265         */
266        private SequenceAccessorService getSequenceAccessorService() {
267            if ( sequenceAccessorService == null ) {
268                sequenceAccessorService = KRADServiceLocator.getSequenceAccessorService();
269            }
270            return sequenceAccessorService;
271        }
272        /**
273         * {@inheritDoc}
274         */
275        @Override
276        public void processAfterNew(MaintenanceDocument document, Map<String, String[]> requestParameters) {
277            super.processAfterNew(document, requestParameters);
278            document.getDocumentHeader().setDocumentDescription(NEW_AGENDA_EDITOR_DOCUMENT_TEXT);
279        }
280    
281        @Override
282        public void processAfterEdit(MaintenanceDocument document, Map<String, String[]> requestParameters) {
283            super.processAfterEdit(document, requestParameters);
284            document.getDocumentHeader().setDocumentDescription("Modify Agenda Editor Document");
285        }
286    
287        @Override
288        public void prepareForSave() {
289            // set agenda attributes
290            AgendaEditor agendaEditor = (AgendaEditor) getDataObject();
291            agendaEditor.getAgenda().setAttributes(agendaEditor.getCustomAttributesMap());
292        }
293    
294        @Override
295        public void saveDataObject() {
296            AgendaBo agendaBo = ((AgendaEditor) getDataObject()).getAgenda();
297    
298            // handle saving new parameterized terms
299            for (AgendaItemBo agendaItem : agendaBo.getItems()) {
300                PropositionBo propositionBo = agendaItem.getRule().getProposition();
301                if (propositionBo != null) {
302                    saveNewParameterizedTerms(propositionBo);
303                }
304            }
305    
306            if (agendaBo instanceof PersistableBusinessObject) {
307                                    
308                Map<String,String> primaryKeys = new HashMap<String, String>();
309                primaryKeys.put("id", agendaBo.getId());
310                AgendaBo blah = getBusinessObjectService().findByPrimaryKey(AgendaBo.class, primaryKeys);
311                
312                    // need to be sure we delete the agenda tree from the top down in order to prevent violating
313                    // any foreign key constraints, so do a pre-order traversal here on each node in the agenda tree
314                    
315                    preOrderTraversalDelete(getFirstAgendaItem(blah));
316                
317                getBusinessObjectService().delete(blah);
318    
319                getBusinessObjectService().linkAndSave(agendaBo);
320            } else {
321                throw new RuntimeException(
322                        "Cannot save object of type: " + agendaBo + " with business object service");
323            }
324        }
325        
326        private AgendaItemBo getFirstAgendaItem(AgendaBo agenda) {
327            String firstItemId = agenda.getFirstItemId();
328            if (firstItemId == null) {
329                    return null;
330            }
331            for (AgendaItemBo item : agenda.getItems()) {
332                    if (item.getId().equals(firstItemId)) {
333                            return item;
334                    }
335            }
336            throw new IllegalStateException("Failed to locate the first agenda item on the agenda with an id of " + firstItemId + ", agenda id is " + agenda.getId());
337        }
338        
339        private void preOrderTraversalDelete(AgendaItemBo agendaItem) {
340            if (agendaItem == null) {
341                    return;
342            }
343            getBusinessObjectService().delete(agendaItem);
344            if (agendaItem.getWhenFalse() != null) {
345                            preOrderTraversalDelete(agendaItem.getWhenFalse());
346                    }
347            if (agendaItem.getWhenTrue() != null) {
348                    preOrderTraversalDelete(agendaItem.getWhenTrue());
349            }       
350            preOrderTraversalDelete(agendaItem.getAlways());
351        }
352            
353        /**
354         * walk the proposition tree and save any new parameterized terms that are contained therein
355         * @param propositionBo the root proposition from which to search
356         */
357        private void saveNewParameterizedTerms(PropositionBo propositionBo) {
358            if (StringUtils.isBlank(propositionBo.getCompoundOpCode())) {
359                // it is a simple proposition
360                String termId = propositionBo.getParameters().get(0).getValue();
361                if (termId.startsWith(KrmsImplConstants.PARAMETERIZED_TERM_PREFIX)) {
362                    String termSpecId = termId.substring(KrmsImplConstants.PARAMETERIZED_TERM_PREFIX.length());
363                    // create new term
364                    TermBo newTerm = new TermBo();
365                    newTerm.setDescription(propositionBo.getNewTermDescription());
366                    newTerm.setSpecificationId(termSpecId);
367                    newTerm.setId(KRADServiceLocator.getSequenceAccessorService().getNextAvailableSequenceNumber("KRMS_TERM_S").toString());
368    
369                    List<TermParameterBo> params = new ArrayList<TermParameterBo>();
370                    for (Map.Entry<String, String> entry : propositionBo.getTermParameters().entrySet()) {
371                        TermParameterBo param = new TermParameterBo();
372                        param.setTermId(newTerm.getId());
373                        param.setName(entry.getKey());
374                        param.setValue(entry.getValue());
375                        param.setId(KRADServiceLocator.getSequenceAccessorService().getNextAvailableSequenceNumber("KRMS_TERM_PARM_S").toString());
376    
377                        params.add(param);
378                    }
379    
380                    newTerm.setParameters(params);
381    
382                    KRADServiceLocator.getBusinessObjectService().linkAndSave(newTerm);
383                    propositionBo.getParameters().get(0).setValue(newTerm.getId());
384                }
385            } else {
386                // recurse
387                for (PropositionBo childProp : propositionBo.getCompoundComponents()) {
388                    saveNewParameterizedTerms(childProp);
389                }
390            }
391        }
392    
393        /**
394         * Build a map from attribute name to attribute definition from all the defined attribute definitions for the
395         * specified agenda type
396         * @param agendaTypeId
397         * @return
398         */
399        private Map<String, KrmsAttributeDefinition> buildAttributeDefinitionMap(String agendaTypeId) {
400            KrmsAttributeDefinitionService attributeDefinitionService = KrmsRepositoryServiceLocator.getKrmsAttributeDefinitionService();
401    
402            // build a map from attribute name to definition
403            Map<String, KrmsAttributeDefinition> attributeDefinitionMap = new HashMap<String, KrmsAttributeDefinition>();
404    
405            List<KrmsAttributeDefinition> attributeDefinitions =
406                    attributeDefinitionService.findAttributeDefinitionsByType(agendaTypeId);
407    
408            for (KrmsAttributeDefinition attributeDefinition : attributeDefinitions) {
409                attributeDefinitionMap.put(attributeDefinition.getName(), attributeDefinition);
410            }
411            return attributeDefinitionMap;
412        }
413    
414        @Override
415        public boolean isOldDataObjectInDocument() {
416            boolean isOldDataObjectInExistence = true;
417    
418            if (getDataObject() == null) {
419                isOldDataObjectInExistence = false;
420            } else {
421                // dataObject contains a non persistable wrapper - use agenda from the wrapper object instead
422                Map<String, ?> keyFieldValues = getDataObjectMetaDataService().getPrimaryKeyFieldValues(((AgendaEditor) getDataObject()).getAgenda());
423                for (Object keyValue : keyFieldValues.values()) {
424                    if (keyValue == null) {
425                        isOldDataObjectInExistence = false;
426                    } else if ((keyValue instanceof String) && StringUtils.isBlank((String) keyValue)) {
427                        isOldDataObjectInExistence = false;
428                    }
429    
430                    if (!isOldDataObjectInExistence) {
431                        break;
432                    }
433                }
434            }
435    
436            return isOldDataObjectInExistence;
437        }
438    
439        // Since the dataObject is a wrapper class we need to return the agendaBo instead.
440        @Override
441        public Class getDataObjectClass() {
442            return AgendaBo.class;
443        }
444    
445        @Override
446        protected void processBeforeAddLine(View view, CollectionGroup collectionGroup, Object model, Object addLine) {
447            AgendaEditor agendaEditor = getAgendaEditor(model);
448            if (addLine instanceof ActionBo) {
449                ((ActionBo) addLine).setNamespace(agendaEditor.getAgendaItemLine().getRule().getNamespace());
450            }
451    
452            super.processBeforeAddLine(view, collectionGroup, model, addLine);
453        }
454    }