001/**
002 * Copyright 2005-2016 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.MaintenanceDocument;
028import org.kuali.rice.krad.maintenance.Maintainable;
029import org.kuali.rice.krad.maintenance.MaintainableImpl;
030import org.kuali.rice.krad.service.BusinessObjectService;
031import org.kuali.rice.krad.service.KRADServiceLocator;
032import org.kuali.rice.krad.service.SequenceAccessorService;
033import org.kuali.rice.krad.uif.container.CollectionGroup;
034import org.kuali.rice.krad.uif.container.Container;
035import org.kuali.rice.krad.uif.view.View;
036import org.kuali.rice.krad.util.KRADConstants;
037import org.kuali.rice.krad.web.form.MaintenanceForm;
038import org.kuali.rice.krms.api.repository.term.TermResolverDefinition;
039import org.kuali.rice.krms.api.repository.type.KrmsAttributeDefinition;
040import org.kuali.rice.krms.impl.repository.ActionBo;
041import org.kuali.rice.krms.impl.repository.AgendaBo;
042import org.kuali.rice.krms.impl.repository.AgendaItemBo;
043import org.kuali.rice.krms.impl.repository.ContextBoService;
044import org.kuali.rice.krms.impl.repository.KrmsAttributeDefinitionService;
045import org.kuali.rice.krms.impl.repository.KrmsRepositoryServiceLocator;
046import org.kuali.rice.krms.impl.repository.PropositionBo;
047import org.kuali.rice.krms.impl.repository.PropositionParameterBo;
048import org.kuali.rice.krms.impl.repository.RuleBo;
049import org.kuali.rice.krms.impl.repository.TermBo;
050import org.kuali.rice.krms.impl.repository.TermParameterBo;
051import org.kuali.rice.krms.impl.util.KrmsImplConstants;
052import org.kuali.rice.krms.impl.util.KrmsRetriever;
053
054import java.util.ArrayList;
055import java.util.Collections;
056import java.util.Date;
057import java.util.HashMap;
058import java.util.List;
059import java.util.Map;
060
061/**
062 * {@link Maintainable} for the {@link AgendaEditor}
063 *
064 * @author Kuali Rice Team (rice.collab@kuali.org)
065 *
066 */
067public 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(termSpecId, namespace);
165
166        TermResolverDefinition simplestResolver = null;
167
168        for (TermResolverDefinition resolver : resolvers) {
169            if (simplestResolver == null ||
170                    simplestResolver.getParameterNames().size() < resolver.getParameterNames().size()) {
171                simplestResolver = resolver;
172            }
173        }
174
175        return simplestResolver;
176    }
177
178    /**
179     * Find and return the node containing the proposition that is in currently in edit mode
180     * @param node the node to start searching from (typically the root)
181     * @return the node that is currently being edited, if any.  Otherwise, null.
182     */
183    private Node<RuleTreeNode, String> findEditedProposition(Node<RuleTreeNode, String> node) {
184        Node<RuleTreeNode, String> result = null;
185        if (node.getData() != null && node.getData().getProposition() != null &&
186                node.getData().getProposition().getEditMode()) {
187            result = node;
188        } else {
189            for (Node<RuleTreeNode, String> child : node.getChildren()) {
190                result = findEditedProposition(child);
191                if (result != null) break;
192            }
193        }
194        return result;
195    }
196
197    /**
198     * Get the AgendaEditor out of the MaintenanceForm's newMaintainableObject
199     * @param model the MaintenanceForm
200     * @return the AgendaEditor
201     */
202    private AgendaEditor getAgendaEditor(Object model) {
203        MaintenanceForm maintenanceForm = (MaintenanceForm)model;
204        return (AgendaEditor)maintenanceForm.getDocument().getNewMaintainableObject().getDataObject();
205    }
206
207    public List<RemotableAttributeField> retrieveRuleActionCustomAttributes(View view, Object model, Container container) {
208        AgendaEditor agendaEditor = getAgendaEditor((MaintenanceForm) model);
209        return krmsRetriever.retrieveRuleActionCustomAttributes(agendaEditor);
210    }
211
212    /**
213     *  This only supports a single action within a rule.
214     */
215    public List<RemotableAttributeField> retrieveRuleCustomAttributes(View view, Object model, Container container) {
216        AgendaEditor agendaEditor = getAgendaEditor((MaintenanceForm) model);
217        return krmsRetriever.retrieveRuleCustomAttributes(agendaEditor);
218    }
219
220    @Override
221    public Object retrieveObjectForEditOrCopy(MaintenanceDocument document, Map<String, String> dataObjectKeys) {
222        Object dataObject = null;
223
224        try {
225            // Since the dataObject is a wrapper class we need to build it and populate with the agenda bo.
226            AgendaEditor agendaEditor = new AgendaEditor();
227            AgendaBo agenda = getLookupService().findObjectBySearch(((AgendaEditor) getDataObject()).getAgenda().getClass(), dataObjectKeys);
228            if (KRADConstants.MAINTENANCE_COPY_ACTION.equals(getMaintenanceAction())) {
229                String dateTimeStamp = (new Date()).getTime() + "";
230                String newAgendaName = AgendaItemBo.COPY_OF_TEXT + agenda.getName() + " " + dateTimeStamp;
231
232                AgendaBo copiedAgenda = agenda.copyAgenda(newAgendaName, dateTimeStamp);
233
234                document.getDocumentHeader().setDocumentDescription(NEW_AGENDA_EDITOR_DOCUMENT_TEXT);
235                document.setFieldsClearedOnCopy(true);
236                agendaEditor.setAgenda(copiedAgenda);
237            } else {
238                // set custom attributes map in AgendaEditor
239                //                agendaEditor.setCustomAttributesMap(agenda.getAttributes());
240                agendaEditor.setAgenda(agenda);
241            }
242            agendaEditor.setCustomAttributesMap(agenda.getAttributes());
243
244
245            // set extra fields on AgendaEditor
246            agendaEditor.setNamespace(agenda.getContext().getNamespace());
247            agendaEditor.setContextName(agenda.getContext().getName());
248
249            dataObject = agendaEditor;
250        } catch (ClassNotPersistenceCapableException ex) {
251            if (!document.getOldMaintainableObject().isExternalBusinessObject()) {
252                throw new RuntimeException("Data Object Class: " + getDataObjectClass() +
253                        " is not persistable and is not externalizable - configuration error");
254            }
255            // otherwise, let fall through
256        }
257
258        return dataObject;
259    }
260
261    /**
262     *  Returns the sequenceAssessorService
263     * @return {@link SequenceAccessorService}
264     */
265    private SequenceAccessorService getSequenceAccessorService() {
266        if ( sequenceAccessorService == null ) {
267            sequenceAccessorService = KRADServiceLocator.getSequenceAccessorService();
268        }
269        return sequenceAccessorService;
270    }
271    /**
272     * {@inheritDoc}
273     */
274    @Override
275    public void processAfterNew(MaintenanceDocument document, Map<String, String[]> requestParameters) {
276        super.processAfterNew(document, requestParameters);
277        document.getDocumentHeader().setDocumentDescription(NEW_AGENDA_EDITOR_DOCUMENT_TEXT);
278    }
279
280    @Override
281    public void processAfterEdit(MaintenanceDocument document, Map<String, String[]> requestParameters) {
282        super.processAfterEdit(document, requestParameters);
283        document.getDocumentHeader().setDocumentDescription("Modify Agenda Editor Document");
284    }
285
286    @Override
287    public void prepareForSave() {
288        // set agenda attributes
289        AgendaEditor agendaEditor = (AgendaEditor) getDataObject();
290        agendaEditor.getAgenda().setAttributes(agendaEditor.getCustomAttributesMap());
291    }
292
293    @Override
294    public void saveDataObject() {
295        AgendaBo agendaBo = ((AgendaEditor) getDataObject()).getAgenda();
296
297        // handle saving new parameterized terms
298        for (AgendaItemBo agendaItem : agendaBo.getItems()) {
299            PropositionBo propositionBo = agendaItem.getRule().getProposition();
300            if (propositionBo != null) {
301                saveNewParameterizedTerms(propositionBo);
302            }
303        }
304
305        if (agendaBo instanceof PersistableBusinessObject) {
306            Map<String,String> primaryKeys = new HashMap<String, String>();
307            primaryKeys.put("id", agendaBo.getId());
308            AgendaBo blah = getBusinessObjectService().findByPrimaryKey(AgendaBo.class, primaryKeys);
309            getBusinessObjectService().delete(blah);
310
311            getBusinessObjectService().linkAndSave(agendaBo);
312        } else {
313            throw new RuntimeException(
314                    "Cannot save object of type: " + agendaBo + " with business object service");
315        }
316    }
317
318    /**
319     * walk the proposition tree and save any new parameterized terms that are contained therein
320     * @param propositionBo the root proposition from which to search
321     */
322    private void saveNewParameterizedTerms(PropositionBo propositionBo) {
323        if (StringUtils.isBlank(propositionBo.getCompoundOpCode())) {
324            // it is a simple proposition
325             if (!propositionBo.getParameters().isEmpty() && propositionBo.getParameters().get(0).getValue().startsWith(KrmsImplConstants.PARAMETERIZED_TERM_PREFIX)) {
326                String termId = propositionBo.getParameters().get(0).getValue();
327                String termSpecId = termId.substring(KrmsImplConstants.PARAMETERIZED_TERM_PREFIX.length());
328                // create new term
329                TermBo newTerm = new TermBo();
330                newTerm.setDescription(propositionBo.getNewTermDescription());
331                newTerm.setSpecificationId(termSpecId);
332                newTerm.setId(KRADServiceLocator.getSequenceAccessorService().getNextAvailableSequenceNumber(
333                        KrmsMaintenanceConstants.Sequences.TERM_SPECIFICATION, TermBo.class).toString());
334
335                List<TermParameterBo> params = new ArrayList<TermParameterBo>();
336                for (Map.Entry<String, String> entry : propositionBo.getTermParameters().entrySet()) {
337                    TermParameterBo param = new TermParameterBo();
338                    param.setTermId(newTerm.getId());
339                    param.setName(entry.getKey());
340                    param.setValue(entry.getValue());
341                    param.setId(KRADServiceLocator.getSequenceAccessorService().getNextAvailableSequenceNumber(
342                            KrmsMaintenanceConstants.Sequences.TERM_PARAMETER, TermParameterBo.class).toString());
343
344                    params.add(param);
345                }
346
347                newTerm.setParameters(params);
348
349                KRADServiceLocator.getBusinessObjectService().linkAndSave(newTerm);
350                propositionBo.getParameters().get(0).setValue(newTerm.getId());
351            }
352        } else {
353            // recurse
354            for (PropositionBo childProp : propositionBo.getCompoundComponents()) {
355                saveNewParameterizedTerms(childProp);
356            }
357        }
358    }
359
360    /**
361     * Build a map from attribute name to attribute definition from all the defined attribute definitions for the
362     * specified agenda type
363     * @param agendaTypeId
364     * @return
365     */
366    private Map<String, KrmsAttributeDefinition> buildAttributeDefinitionMap(String agendaTypeId) {
367        KrmsAttributeDefinitionService attributeDefinitionService = KrmsRepositoryServiceLocator.getKrmsAttributeDefinitionService();
368
369        // build a map from attribute name to definition
370        Map<String, KrmsAttributeDefinition> attributeDefinitionMap = new HashMap<String, KrmsAttributeDefinition>();
371
372        List<KrmsAttributeDefinition> attributeDefinitions =
373                attributeDefinitionService.findAttributeDefinitionsByType(agendaTypeId);
374
375        for (KrmsAttributeDefinition attributeDefinition : attributeDefinitions) {
376            attributeDefinitionMap.put(attributeDefinition.getName(), attributeDefinition);
377        }
378        return attributeDefinitionMap;
379    }
380
381    @Override
382    public boolean isOldDataObjectInDocument() {
383        boolean isOldDataObjectInExistence = true;
384
385        if (getDataObject() == null) {
386            isOldDataObjectInExistence = false;
387        } else {
388            // dataObject contains a non persistable wrapper - use agenda from the wrapper object instead
389            Map<String, ?> keyFieldValues = getDataObjectMetaDataService().getPrimaryKeyFieldValues(((AgendaEditor) getDataObject()).getAgenda());
390            for (Object keyValue : keyFieldValues.values()) {
391                if (keyValue == null) {
392                    isOldDataObjectInExistence = false;
393                } else if ((keyValue instanceof String) && StringUtils.isBlank((String) keyValue)) {
394                    isOldDataObjectInExistence = false;
395                }
396
397                if (!isOldDataObjectInExistence) {
398                    break;
399                }
400            }
401        }
402
403        return isOldDataObjectInExistence;
404    }
405
406    // Since the dataObject is a wrapper class we need to return the agendaBo instead.
407    @Override
408    public Class getDataObjectClass() {
409        return AgendaBo.class;
410    }
411
412    @Override
413    public boolean isLockable() {
414        return true;
415    }
416
417    @Override
418    public PersistableBusinessObject getPersistableBusinessObject() {
419        return ((AgendaEditor) getDataObject()).getAgenda();
420    }
421
422    @Override
423    protected void processBeforeAddLine(View view, CollectionGroup collectionGroup, Object model, Object addLine) {
424        AgendaEditor agendaEditor = getAgendaEditor(model);
425        if (addLine instanceof ActionBo) {
426            ((ActionBo) addLine).setNamespace(agendaEditor.getAgendaItemLine().getRule().getNamespace());
427        }
428
429        super.processBeforeAddLine(view, collectionGroup, model, addLine);
430    }
431}