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.framework.engine;
017
018 import java.util.ArrayList;
019 import java.util.Arrays;
020 import java.util.Collection;
021 import java.util.Collections;
022 import java.util.HashMap;
023 import java.util.HashSet;
024 import java.util.Iterator;
025 import java.util.LinkedList;
026 import java.util.List;
027 import java.util.Map;
028 import java.util.Map.Entry;
029 import java.util.PriorityQueue;
030 import java.util.Set;
031
032 import org.apache.commons.collections.CollectionUtils;
033 import org.apache.commons.lang.ArrayUtils;
034 import org.apache.commons.lang.StringUtils;
035 import org.kuali.rice.krms.api.engine.Term;
036 import org.kuali.rice.krms.api.engine.TermResolutionEngine;
037 import org.kuali.rice.krms.api.engine.TermResolutionException;
038 import 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 */
044 public 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