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}