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 }