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