001/** 002 * Copyright 2005-2015 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.framework.engine; 017 018import java.util.ArrayList; 019import java.util.Arrays; 020import java.util.Collection; 021import java.util.Collections; 022import java.util.HashMap; 023import java.util.HashSet; 024import java.util.Iterator; 025import java.util.LinkedList; 026import java.util.List; 027import java.util.Map; 028import java.util.Map.Entry; 029import java.util.PriorityQueue; 030import java.util.Set; 031 032import org.apache.commons.collections.CollectionUtils; 033import org.apache.commons.lang.ArrayUtils; 034import org.apache.commons.lang.StringUtils; 035import org.kuali.rice.krms.api.engine.Term; 036import org.kuali.rice.krms.api.engine.TermResolutionEngine; 037import org.kuali.rice.krms.api.engine.TermResolutionException; 038import org.kuali.rice.krms.api.engine.TermResolver; 039 040/** 041 * An implementation of {@link TermResolutionEngine} 042 * @author Kuali Rice Team (rice.collab@kuali.org) 043 */ 044public class TermResolutionEngineImpl implements TermResolutionEngine { 045 private static final org.apache.log4j.Logger LOG = org.apache.log4j.Logger.getLogger(TermResolutionEngineImpl.class); 046 047 private final Map<String, List<TermResolver<?>>> termResolversByOutput = new HashMap<String, List<TermResolver<?>>>(); 048 private final Map<TermResolverKey, TermResolver<?>> termResolversByKey = new HashMap<TermResolverKey, TermResolver<?>>(); 049 050 // should this use soft refs? Will require some refactoring to check if the referenced object is around; 051 private final Map<Term, Object> termCache = new HashMap<Term, Object>(); 052 053 @Override 054 public void addTermValue(Term term, Object value) { 055 termCache.put(term, value); 056 } 057 058 @Override 059 public void addTermResolver(TermResolver<?> termResolver) { 060 if (termResolver == null) throw new IllegalArgumentException("termResolver is reuqired"); 061 if (termResolver.getOutput() == null) throw new IllegalArgumentException("termResolver.getOutput() must not be null"); 062 063 List<TermResolver<?>> termResolvers = termResolversByOutput.get(termResolver.getOutput()); 064 if (termResolvers == null) { 065 termResolvers = new LinkedList<TermResolver<?>>(); 066 termResolversByOutput.put(termResolver.getOutput(), termResolvers); 067 } 068 termResolversByKey.put(new TermResolverKey(termResolver), termResolver); 069 termResolvers.add(termResolver); 070 } 071 072 @SuppressWarnings("unchecked") 073 @Override 074 public <T> T resolveTerm(Term term) throws TermResolutionException { 075 LOG.debug("+--> resolveTerm(" + term + ")"); 076 if (termCache.containsKey(term)) return (T)termCache.get(term); 077 078 String termName = term.getName(); 079 080 // build plan w/ termName spec for correct TermResolver selection 081 List<TermResolverKey> resolutionPlan = buildTermResolutionPlan(termName); 082 // TODO: could cache these plans somewhere, since future agendas will likely require the same plans 083 084 LOG.debug("resolutionPlan: " + (resolutionPlan == null ? "null" : StringUtils.join(resolutionPlan.iterator(), ", "))); 085 086 if (resolutionPlan != null) { 087 LOG.debug("executing plan"); 088 for (TermResolverKey resolverKey : resolutionPlan) { 089 TermResolver<?> resolver = termResolversByKey.get(resolverKey); 090 091 // build prereqs 092 Map<String, Object> resolvedPrereqs = new HashMap<String, Object>(); 093 094 // The plan order should guarantee these prereqs exist and are cached. 095 for (String prereq : resolver.getPrerequisites()) { 096 Object resolvedPrereq = termCache.get(new Term(prereq, null)); 097 resolvedPrereqs.put(prereq, resolvedPrereq); 098 } 099 100 Map<String, String> providedParameters = Collections.emptyMap(); 101 // The destination Term (and only the dest Term) can be parameterized 102 if (termName.equals(resolver.getOutput())) { 103 providedParameters = term.getParameters(); 104 105 // throw a TermResolutionException if the params doen't match up 106 validateTermParameters(resolver, providedParameters); 107 } 108 109 Object resolvedTerm = resolver.resolve(resolvedPrereqs, providedParameters); 110 if (termName.equals(resolver.getOutput())) { 111 termCache.put(term, resolvedTerm); 112 } else { 113 if (!CollectionUtils.isEmpty(resolver.getParameterNames())) { 114 // Shouldn't happen due to checks in buildResolutionPlan 115 throw new TermResolutionException("TermResolvers requiring parameters cannot be intermediates in the Term resolution plan", resolver, providedParameters); 116 } 117 termCache.put(new Term(resolver.getOutput(), null), resolvedTerm); 118 } 119 } 120 } else { 121 throw new TermResolutionException("Unable to plan the resolution of " + term, null, null); 122 } 123 return (T)termCache.get(term); 124 } 125 126 /** 127 * This method checks that the required parameters (as returned by the {@link TermResolver} via 128 * {@link TermResolver#getParameterNames()}) are met in the {@link Map} of provided parameters. 129 * 130 * @param resolver {@link TermResolver} 131 * @param providedParameters 132 * @throws TermResolutionException if provided parameters do not match requirements 133 */ 134 private void validateTermParameters(TermResolver<?> resolver, 135 Map<String, String> providedParameters) 136 throws TermResolutionException { 137 // validate that params match what the TermResolver requires 138 if (!providedParameters.keySet().equals(resolver.getParameterNames())) { 139 StringBuilder sb = new StringBuilder(); 140 141 boolean first = true; 142 for (Entry<String,String> param : providedParameters.entrySet()) { 143 if (first) { 144 first = false; 145 } else { 146 sb.append(","); 147 } 148 sb.append(param.getKey()); 149 sb.append("="); 150 sb.append(param.getValue()); 151 } 152 153 throw new TermResolutionException("provided parameters ("+ sb 154 +") do not match requirements (" 155 + StringUtils.join(resolver.getParameterNames(), ",") +")", resolver, providedParameters); 156 } 157 } 158 159 /** 160 * 161 * @param termName 162 * @return List<{@link TermResolverKey}> 163 */ 164 protected List<TermResolverKey> buildTermResolutionPlan(String termName) { 165 // our result 166 List<TermResolverKey> resolutionPlan = null; 167 168 // Holds the resolvers we've visited, along with the needed metadata for generating our final plan 169 Map<TermResolverKey, Visited> visitedByKey = new HashMap<TermResolverKey, Visited>(); 170 171 // this holds a least cost first list of nodes remaining to be explored 172 PriorityQueue<ToVisit> toVisits = new PriorityQueue<ToVisit>(); // nice grammar there cowboy 173 174 // dummy resolver to be the root of this tree 175 // Do I really need this? Yes, because there may be more than one resolver that resolves to the desired termName, 176 // so this destination unifies the trees of those candidate resolvers 177 TermResolver destination = createDestination(termName); // problem is we can't get this one out of the registry 178 TermResolverKey destinationKey = new TermResolverKey(destination); 179 180 LOG.debug("Beginning resolution tree search for " + termName); 181 182 // seed our queue of resolvers to visit 183 // need to be aware of null parent for root ToVisit 184 toVisits.add(new ToVisit(0, destination, null)); 185 186 // there may not be a viable plan 187 boolean plannedToDestination = false; 188 189 // We'll do a modified Dijkstra's shortest path algorithm, where at each leaf we see if we've planned out 190 // termName resolution all the way up to the root, our destination. If so, we just reconstruct our plan. 191 while (!plannedToDestination && toVisits.size() > 0) { 192 // visit least cost node remaining 193 ToVisit visiting = toVisits.poll(); 194 195 LOG.debug("visiting " + visiting.getTermResolverKey()); 196 197 // the resolver is the edge in our tree -- we don't get it directly from the termResolversByKey Map, because it could be our destination 198 TermResolver resolver = getResolver(visiting.getTermResolverKey(), destination, destinationKey); 199 TermResolver parent = getResolver(visiting.getParentKey(), destination, destinationKey); 200 201 if (visitedByKey.containsKey(visiting.getTermResolverKey())) { 202 continue; // We've already visited this one 203 } 204 205 Visited parentVisited = visitedByKey.get(visiting.getParentKey()); 206 207 if (resolver == null) throw new RuntimeException("Unable to get TermResolver by its key"); 208 Set<String> prereqs = resolver.getPrerequisites(); 209 // keep track of any prereqs that we already have handy 210 List<String> metPrereqs = new LinkedList<String>(); 211 212 // see what prereqs we have already, and which we'll need to visit 213 if (prereqs != null) for (String prereq : prereqs) { 214 if (!termCache.containsKey(new Term(prereq, null))) { 215 // enqueue all resolvers in toVisits 216 List<TermResolver<?>> prereqResolvers = termResolversByOutput.get(prereq); 217 if (prereqResolvers != null) for (TermResolver prereqResolver : prereqResolvers) { 218 // Only TermResolvers that don't take paramaterized terms can be chained, so: 219 // if the TermResolver doesn't take parameters, or it resolves the output termName 220 if (CollectionUtils.isEmpty(prereqResolver.getParameterNames()) || 221 termName.equals(prereqResolver.getOutput())) 222 { 223 // queue it up for visiting 224 toVisits.add(new ToVisit(visiting.getCost() /* cost to get to this resolver */, prereqResolver, resolver)); 225 } 226 } 227 } else { 228 metPrereqs.add(prereq); 229 } 230 } 231 232 // Build visited info 233 Visited visited = buildVisited(resolver, parentVisited, metPrereqs); 234 visitedByKey.put(visited.getResolverKey(), visited); 235 236 plannedToDestination = isPlannedBackToDestination(visited, destinationKey, visitedByKey); 237 } 238 239 if (plannedToDestination) { 240 // build result from Visited tree. 241 resolutionPlan = new LinkedList<TermResolverKey>(); 242 243 assembleLinearResolutionPlan(visitedByKey.get(destinationKey), visitedByKey, resolutionPlan); 244 } 245 return resolutionPlan; 246 } 247 248 /** 249 * @return the Visited object for the resolver we just, er, well, visited. 250 */ 251 private Visited buildVisited(TermResolver resolver, Visited parentVisited, Collection<String> metPrereqs) { 252 Visited visited = null; 253 254 List<TermResolverKey> pathTo = new ArrayList<TermResolverKey>(1 + (parentVisited == null ? 0 : parentVisited.pathTo.size())); 255 if (parentVisited != null && parentVisited.getPathTo() != null) pathTo.addAll(parentVisited.getPathTo()); 256 if (parentVisited != null) pathTo.add(parentVisited.getResolverKey()); 257 TermResolverKey resolverKey = new TermResolverKey(resolver); 258 259 visited = new Visited(resolverKey, pathTo, resolver.getPrerequisites(), resolver.getCost() + (parentVisited == null ? 0 : parentVisited.getCost())); 260 for (String metPrereq : metPrereqs) { visited.addPlannedPrereq(metPrereq); } 261 262 return visited; 263 } 264 265 /** 266 * our dummy destination isn't allowed to pollute termResolversByKey, hence the ugly conditional encapsulated herein 267 */ 268 private TermResolver getResolver(TermResolverKey resolverKey, 269 TermResolver destination, TermResolverKey destinationKey) { 270 TermResolver resolver; 271 if (destinationKey.equals(resolverKey)) { 272 resolver = destination; 273 } else { 274 resolver = termResolversByKey.get(resolverKey); 275 } 276 return resolver; 277 } 278 279 /** 280 * @param visited 281 * @param destinationKey 282 * @param visitedByKey 283 * @return 284 */ 285 private boolean isPlannedBackToDestination(Visited visited, 286 TermResolverKey destinationKey, 287 Map<TermResolverKey, Visited> visitedByKey) { 288 boolean plannedToDestination = false; 289 if (visited.isFullyPlanned()) { 290 LOG.debug("Leaf! this resolver's prereqs are all avialable."); 291 // no traversing further yet, instead we need to check how far up the tree is fully planned out 292 // step backwards toward the root of the tree and see if we're good all the way to our objective. 293 // if a node fully planned, move up the tree (towards the root) to see if its parent is fully planned, and so on. 294 295 if (visited.getPathTo().size() > 0) { 296 // reverse the path to 297 List<TermResolverKey> reversePathTo = new ArrayList<TermResolverKey>(visited.getPathTo()); 298 Collections.reverse(reversePathTo); 299 300 // we use this to propagate resolutions up the tree 301 Visited previousAncestor = visited; 302 303 for (TermResolverKey ancestorKey : reversePathTo) { 304 305 Visited ancestorVisited = visitedByKey.get(ancestorKey); 306 ancestorVisited.addPlannedPrereq(previousAncestor.getResolverKey()); 307 308 LOG.debug("checking ancestor " + ancestorKey); 309 310 if (ancestorVisited.isFullyPlanned() && ancestorKey.equals(destinationKey)) { 311 // Woot! Job's done! 312 plannedToDestination = true; 313 break; 314 } else if (!ancestorVisited.isFullyPlanned()) { // if the ancestor isn't fully planned, we're done 315 LOG.debug("Still have planning to do."); 316 break; 317 } 318 // update previousAncestor reference for next iteration 319 previousAncestor = ancestorVisited; 320 } 321 } else { 322 // we're done already! do a jig? 323 LOG.debug("Trivial plan."); 324 plannedToDestination = true; 325 } 326 } 327 return plannedToDestination; 328 } 329 330 /** 331 * 332 * @param visited 333 * @param visitedByKey 334 * @param plan 335 */ 336 private void assembleLinearResolutionPlan(Visited visited, Map<TermResolverKey, Visited> visitedByKey, List<TermResolverKey> plan) { 337 // DFS 338 for (TermResolverKey prereqResolverKey : visited.getPrereqResolvers()) { 339 Visited prereqVisited = visitedByKey.get(prereqResolverKey); 340 assembleLinearResolutionPlan(prereqVisited, visitedByKey, plan); 341 plan.add(prereqResolverKey); 342 } 343 } 344 345 /** 346 * Create our dummy destination resolver 347 * @param termName 348 */ 349 private TermResolver<? extends Object> createDestination(final String termName) { 350 TermResolver<?> destination = new TermResolver<Object>() { 351 final String dest = "termResolutionEngineDestination"; 352 @Override 353 public int getCost() { 354 return 0; 355 } 356 @Override 357 public String getOutput() { 358 return dest; 359 } 360 @Override 361 public Set<String> getPrerequisites() { 362 return Collections.<String>singleton(termName); 363 } 364 @Override 365 public Set<String> getParameterNames() { 366 return Collections.emptySet(); 367 } 368 @Override 369 public Object resolve(Map<String, Object> resolvedPrereqs, Map<String, String> parameters) throws TermResolutionException { 370 return null; 371 } 372 }; 373 return destination; 374 } 375 376 private static class ToVisit implements Comparable<ToVisit> { 377 378 private final int precost; 379 private final int addcost; 380 private final TermResolverKey resolverKey; 381 382 // the parent key is not being used for comparison purposes currently 383 private final TermResolverKey parentKey; 384 385 /** 386 * @param precost 387 * @param resolver 388 */ 389 public ToVisit(int precost, TermResolver resolver, TermResolver parent) { 390 super(); 391 this.precost = precost; 392 this.addcost = resolver.getCost(); 393 this.resolverKey = new TermResolverKey(resolver); 394 395 if (parent != null) { 396 this.parentKey = new TermResolverKey(parent); 397 } else { 398 this.parentKey = null; 399 } 400 } 401 402 /** 403 * Get the precost plus the addcost 404 * @return in precost plus addcost 405 */ 406 public int getCost() { 407 return precost + addcost; 408 } 409 410 @Override 411 public boolean equals(Object obj) { 412 if (this == obj) return true; 413 if (obj == null) return false; 414 if (getClass() != obj.getClass()) 415 return false; 416 ToVisit other = (ToVisit)obj; 417 return this.compareTo(other) == 0; 418 } 419 420 /* (non-Javadoc) 421 * @see java.lang.Object#hashCode() 422 */ 423 @Override 424 public int hashCode() { 425 final int prime = 31; 426 int result = 1; 427 result = prime * result + getCost(); 428 result = prime * result + ((resolverKey == null) ? 0 : resolverKey.hashCode()); 429 return result; 430 } 431 432 /** 433 * {@inheritDoc Comparable} 434 */ 435 @Override 436 public int compareTo(ToVisit o) { 437 if (o == null) return 1; 438 if (getCost() > o.getCost()) return 1; 439 if (getCost() < o.getCost()) return -1; 440 return resolverKey.compareTo(o.resolverKey); 441 } 442 443 /** 444 * Return the {@link TermResolverKey} 445 * @return {@link TermResolverKey} 446 */ 447 public TermResolverKey getTermResolverKey() { 448 return resolverKey; 449 } 450 451 /** 452 * Return the Parent {@link TermResolverKey} 453 * @return the Parent {@link TermResolverKey} 454 */ 455 public TermResolverKey getParentKey() { 456 return parentKey; 457 } 458 459 @Override 460 public String toString() { 461 return getClass().getSimpleName()+"("+getTermResolverKey()+")"; 462 } 463 } 464 465 protected static class TermResolverKey implements Comparable<TermResolverKey> { 466 private final List<String> data; 467 private final String [] params; 468 469 // just used for toArray call 470 private static final String[] TERM_SPEC_TYPER = new String[0]; 471 private static final String[] STRING_TYPER = new String[0]; 472 473 public TermResolverKey(TermResolver resolver) { 474 this(resolver.getOutput(), resolver.getParameterNames(), resolver.getPrerequisites()); 475 } 476 477 private TermResolverKey(String dest, Set<String> paramSet, Set<String> prereqs) { 478 if (dest == null) throw new IllegalArgumentException("dest parameter must not be null"); 479 data = new ArrayList<String>(1 + ((prereqs == null) ? 0 : prereqs.size())); // size ArrayList perfectly 480 data.add(dest); 481 if (!CollectionUtils.isEmpty(paramSet)) { 482 params = paramSet.toArray(STRING_TYPER); 483 Arrays.sort(params); 484 } else { 485 params = STRING_TYPER; // just assigning a handy zero length String array 486 } 487 if (prereqs != null) { 488 // this is painful, but to be able to compare we need a defined order 489 String [] prereqsArray = prereqs.toArray(TERM_SPEC_TYPER); 490 Arrays.sort(prereqsArray); 491 for (String prereq : prereqsArray) { 492 data.add(prereq); 493 } 494 } 495 } 496 497 public String getOutput() { 498 return data.get(0); 499 } 500 501 @Override 502 public int compareTo(TermResolverKey o) { 503 if (o == null) return 1; 504 505 Iterator<String> mDataIter = data.iterator(); 506 Iterator<String> oDataIter = o.data.iterator(); 507 508 while (mDataIter.hasNext() && oDataIter.hasNext()) { 509 int itemCompareResult = mDataIter.next().compareTo(oDataIter.next()); 510 if (itemCompareResult != 0) return itemCompareResult; 511 } 512 513 if (mDataIter.hasNext()) return 1; 514 if (oDataIter.hasNext()) return -1; 515 516 return 0; 517 } 518 519 /* (non-Javadoc) 520 * @see java.lang.Object#hashCode() 521 */ 522 @Override 523 public int hashCode() { 524 return data.hashCode(); 525 } 526 527 /* (non-Javadoc) 528 * @see java.lang.Object#equals(java.lang.Object) 529 */ 530 @Override 531 public boolean equals(Object obj) { 532 if (this == obj) 533 return true; 534 if (obj == null) 535 return false; 536 if (getClass() != obj.getClass()) 537 return false; 538 TermResolverKey other = (TermResolverKey) obj; 539 return this.compareTo(other) == 0; 540 } 541 542 @Override 543 public String toString() { 544 StringBuilder sb = new StringBuilder(); 545 sb.append(getClass().getSimpleName()); 546 sb.append("("); 547 Iterator<String> iter = data.iterator(); 548 sb.append(iter.next().toString()); 549 if (params.length != 0) { 550 sb.append("+"); 551 ArrayUtils.toString(params); 552 } 553 554 if (iter.hasNext()) sb.append(" <- "); 555 boolean first = true; 556 while (iter.hasNext()) { 557 if (first) first = false; 558 else sb.append(", "); 559 sb.append(iter.next().toString()); 560 } 561 562 563 return sb.toString(); 564 } 565 } 566 567 568 private static class Visited { 569 private TermResolverKey resolverKey; 570 private List<TermResolverKey> pathTo; 571 private Set<String> remainingPrereqs; 572 private Map<String, TermResolverKey> prereqResolvers; 573 private int cost; 574 575 /** 576 * @param resolverKey 577 * @param pathTo 578 * @param prereqs 579 * @param cost 580 */ 581 public Visited(TermResolverKey resolverKey, List<TermResolverKey> pathTo, Set<String> prereqs, int cost) { 582 super(); 583 this.resolverKey = resolverKey; 584 this.pathTo = pathTo; 585 this.remainingPrereqs = new HashSet<String>(prereqs); 586 this.prereqResolvers = new HashMap<String, TermResolverKey>(); 587 this.cost = cost; 588 } 589 590 /** 591 * @return the path from the goal node down to this resolver 592 */ 593 public List<TermResolverKey> getPathTo() { 594 return pathTo; 595 } 596 597 public TermResolverKey getResolverKey() { 598 return resolverKey; 599 } 600 601 public Collection<TermResolverKey> getPrereqResolvers() { 602 return prereqResolvers.values(); 603 } 604 605 /** 606 * @return true if resolution of all the prerequisites has been planned 607 */ 608 public boolean isFullyPlanned() { 609 return remainingPrereqs.isEmpty(); 610 } 611 612 public int getCost() { 613 return cost; 614 } 615 616 public void addPlannedPrereq(TermResolverKey termResolverKey) { 617 remainingPrereqs.remove(termResolverKey.getOutput()); 618 prereqResolvers.put(termResolverKey.getOutput(), termResolverKey); 619 } 620 621 public void addPlannedPrereq(String termName) { 622 remainingPrereqs.remove(termName); 623 } 624 } 625 626 private static class InvalidResolutionPathException extends Exception { 627 private static final long serialVersionUID = 1L; 628 629 public InvalidResolutionPathException() { 630 } 631 } 632 633} 634 635