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