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