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.MaintenanceForm;
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().getTermResolversByOutputId(
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 MaintenanceForm's newMaintainableObject
200 * @param model the MaintenanceForm
201 * @return the AgendaEditor
202 */
203 private AgendaEditor getAgendaEditor(Object model) {
204 MaintenanceForm maintenanceForm = (MaintenanceForm)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((MaintenanceForm) 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((MaintenanceForm) 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 }