1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16 package org.kuali.rice.krms.framework.engine;
17
18 import java.util.ArrayList;
19 import java.util.Arrays;
20 import java.util.Collection;
21 import java.util.Collections;
22 import java.util.HashMap;
23 import java.util.HashSet;
24 import java.util.Iterator;
25 import java.util.LinkedList;
26 import java.util.List;
27 import java.util.Map;
28 import java.util.Map.Entry;
29 import java.util.PriorityQueue;
30 import java.util.Set;
31
32 import org.apache.commons.collections.CollectionUtils;
33 import org.apache.commons.lang.ArrayUtils;
34 import org.apache.commons.lang.StringUtils;
35 import org.kuali.rice.krms.api.engine.Term;
36 import org.kuali.rice.krms.api.engine.TermResolutionEngine;
37 import org.kuali.rice.krms.api.engine.TermResolutionException;
38 import org.kuali.rice.krms.api.engine.TermResolver;
39
40 public class TermResolutionEngineImpl implements TermResolutionEngine {
41 private static final org.apache.log4j.Logger LOG = org.apache.log4j.Logger.getLogger(TermResolutionEngineImpl.class);
42
43 private final Map<String, List<TermResolver<?>>> termResolversByOutput = new HashMap<String, List<TermResolver<?>>>();
44 private final Map<TermResolverKey, TermResolver<?>> termResolversByKey = new HashMap<TermResolverKey, TermResolver<?>>();
45
46
47 private final Map<Term, Object> termCache = new HashMap<Term, Object>();
48
49 @Override
50 public void addTermValue(Term term, Object value) {
51 termCache.put(term, value);
52 }
53
54 @Override
55 public void addTermResolver(TermResolver<?> termResolver) {
56 if (termResolver == null) throw new IllegalArgumentException("termResolver is reuqired");
57 if (termResolver.getOutput() == null) throw new IllegalArgumentException("termResolver.getOutput() must not be null");
58
59 List<TermResolver<?>> termResolvers = termResolversByOutput.get(termResolver.getOutput());
60 if (termResolvers == null) {
61 termResolvers = new LinkedList<TermResolver<?>>();
62 termResolversByOutput.put(termResolver.getOutput(), termResolvers);
63 }
64 termResolversByKey.put(new TermResolverKey(termResolver), termResolver);
65 termResolvers.add(termResolver);
66 }
67
68 @SuppressWarnings("unchecked")
69 @Override
70 public <T> T resolveTerm(Term term) throws TermResolutionException {
71 LOG.debug("+--> resolveTerm(" + term + ")");
72 if (termCache.containsKey(term)) return (T)termCache.get(term);
73
74 String termName = term.getName();
75
76
77 List<TermResolverKey> resolutionPlan = buildTermResolutionPlan(termName);
78
79
80 LOG.debug("resolutionPlan: " + (resolutionPlan == null ? "null" : StringUtils.join(resolutionPlan.iterator(), ", ")));
81
82 if (resolutionPlan != null) {
83 LOG.debug("executing plan");
84 for (TermResolverKey resolverKey : resolutionPlan) {
85 TermResolver<?> resolver = termResolversByKey.get(resolverKey);
86
87
88 Map<String, Object> resolvedPrereqs = new HashMap<String, Object>();
89
90
91 for (String prereq : resolver.getPrerequisites()) {
92 Object resolvedPrereq = termCache.get(new Term(prereq, null));
93 resolvedPrereqs.put(prereq, resolvedPrereq);
94 }
95
96 Map<String, String> providedParameters = Collections.emptyMap();
97
98 if (termName.equals(resolver.getOutput())) {
99 providedParameters = term.getParameters();
100
101
102 validateTermParameters(resolver, providedParameters);
103 }
104
105 Object resolvedTerm = resolver.resolve(resolvedPrereqs, providedParameters);
106 if (termName.equals(resolver.getOutput())) {
107 termCache.put(term, resolvedTerm);
108 } else {
109 if (!CollectionUtils.isEmpty(resolver.getParameterNames())) {
110
111 throw new TermResolutionException("TermResolvers requiring parameters cannot be intermediates in the Term resolution plan", resolver, providedParameters);
112 }
113 termCache.put(new Term(resolver.getOutput(), null), resolvedTerm);
114 }
115 }
116 } else {
117 throw new TermResolutionException("Unable to plan the resolution of " + term, null, null);
118 }
119 return (T)termCache.get(term);
120 }
121
122
123
124
125
126
127
128
129
130 private void validateTermParameters(TermResolver<?> resolver,
131 Map<String, String> providedParameters)
132 throws TermResolutionException {
133
134 if (!providedParameters.keySet().equals(resolver.getParameterNames())) {
135 StringBuilder sb = new StringBuilder();
136
137 boolean first = true;
138 for (Entry<String,String> param : providedParameters.entrySet()) {
139 if (first) {
140 first = false;
141 } else {
142 sb.append(",");
143 }
144 sb.append(param.getKey());
145 sb.append("=");
146 sb.append(param.getValue());
147 }
148
149 throw new TermResolutionException("provided parameters ("+ sb
150 +") do not match requirements ("
151 + StringUtils.join(resolver.getParameterNames(), ",") +")", resolver, providedParameters);
152 }
153 }
154
155 protected List<TermResolverKey> buildTermResolutionPlan(String termName) {
156
157 List<TermResolverKey> resolutionPlan = null;
158
159
160 Map<TermResolverKey, Visited> visitedByKey = new HashMap<TermResolverKey, Visited>();
161
162
163 PriorityQueue<ToVisit> toVisits = new PriorityQueue<ToVisit>();
164
165
166
167
168 TermResolver destination = createDestination(termName);
169 TermResolverKey destinationKey = new TermResolverKey(destination);
170
171 LOG.debug("Beginning resolution tree search for " + termName);
172
173
174
175 toVisits.add(new ToVisit(0, destination, null));
176
177
178 boolean plannedToDestination = false;
179
180
181
182 while (!plannedToDestination && toVisits.size() > 0) {
183
184 ToVisit visiting = toVisits.poll();
185
186 LOG.debug("visiting " + visiting.getTermResolverKey());
187
188
189 TermResolver resolver = getResolver(visiting.getTermResolverKey(), destination, destinationKey);
190 TermResolver parent = getResolver(visiting.getParentKey(), destination, destinationKey);
191
192 if (visitedByKey.containsKey(visiting.getTermResolverKey())) {
193 continue;
194 }
195
196 Visited parentVisited = visitedByKey.get(visiting.getParentKey());
197
198 if (resolver == null) throw new RuntimeException("Unable to get TermResolver by its key");
199 Set<String> prereqs = resolver.getPrerequisites();
200
201 List<String> metPrereqs = new LinkedList<String>();
202
203
204 if (prereqs != null) for (String prereq : prereqs) {
205 if (!termCache.containsKey(new Term(prereq, null))) {
206
207 List<TermResolver<?>> prereqResolvers = termResolversByOutput.get(prereq);
208 if (prereqResolvers != null) for (TermResolver prereqResolver : prereqResolvers) {
209
210
211 if (CollectionUtils.isEmpty(prereqResolver.getParameterNames()) ||
212 termName.equals(prereqResolver.getOutput()))
213 {
214
215 toVisits.add(new ToVisit(visiting.getCost()
216 }
217 }
218 } else {
219 metPrereqs.add(prereq);
220 }
221 }
222
223
224 Visited visited = buildVisited(resolver, parentVisited, metPrereqs);
225 visitedByKey.put(visited.getResolverKey(), visited);
226
227 plannedToDestination = isPlannedBackToDestination(visited, destinationKey, visitedByKey);
228 }
229
230 if (plannedToDestination) {
231
232 resolutionPlan = new LinkedList<TermResolverKey>();
233
234 assembleLinearResolutionPlan(visitedByKey.get(destinationKey), visitedByKey, resolutionPlan);
235 }
236 return resolutionPlan;
237 }
238
239
240
241
242 private Visited buildVisited(TermResolver resolver, Visited parentVisited, Collection<String> metPrereqs) {
243 Visited visited = null;
244
245 List<TermResolverKey> pathTo = new ArrayList<TermResolverKey>(1 + (parentVisited == null ? 0 : parentVisited.pathTo.size()));
246 if (parentVisited != null && parentVisited.getPathTo() != null) pathTo.addAll(parentVisited.getPathTo());
247 if (parentVisited != null) pathTo.add(parentVisited.getResolverKey());
248 TermResolverKey resolverKey = new TermResolverKey(resolver);
249
250 visited = new Visited(resolverKey, pathTo, resolver.getPrerequisites(), resolver.getCost() + (parentVisited == null ? 0 : parentVisited.getCost()));
251 for (String metPrereq : metPrereqs) { visited.addPlannedPrereq(metPrereq); }
252
253 return visited;
254 }
255
256
257
258
259 private TermResolver getResolver(TermResolverKey resolverKey,
260 TermResolver destination, TermResolverKey destinationKey) {
261 TermResolver resolver;
262 if (destinationKey.equals(resolverKey)) {
263 resolver = destination;
264 } else {
265 resolver = termResolversByKey.get(resolverKey);
266 }
267 return resolver;
268 }
269
270
271
272
273
274
275
276 private boolean isPlannedBackToDestination(Visited visited,
277 TermResolverKey destinationKey,
278 Map<TermResolverKey, Visited> visitedByKey) {
279 boolean plannedToDestination = false;
280 if (visited.isFullyPlanned()) {
281 LOG.debug("Leaf! this resolver's prereqs are all avialable.");
282
283
284
285
286 if (visited.getPathTo().size() > 0) {
287
288 List<TermResolverKey> reversePathTo = new ArrayList<TermResolverKey>(visited.getPathTo());
289 Collections.reverse(reversePathTo);
290
291
292 Visited previousAncestor = visited;
293
294 for (TermResolverKey ancestorKey : reversePathTo) {
295
296 Visited ancestorVisited = visitedByKey.get(ancestorKey);
297 ancestorVisited.addPlannedPrereq(previousAncestor.getResolverKey());
298
299 LOG.debug("checking ancestor " + ancestorKey);
300
301 if (ancestorVisited.isFullyPlanned() && ancestorKey.equals(destinationKey)) {
302
303 plannedToDestination = true;
304 break;
305 } else if (!ancestorVisited.isFullyPlanned()) {
306 LOG.debug("Still have planning to do.");
307 break;
308 }
309
310 previousAncestor = ancestorVisited;
311 }
312 } else {
313
314 LOG.debug("Trivial plan.");
315 plannedToDestination = true;
316 }
317 }
318 return plannedToDestination;
319 }
320
321 private void assembleLinearResolutionPlan(Visited visited, Map<TermResolverKey, Visited> visitedByKey, List<TermResolverKey> plan) {
322
323 for (TermResolverKey prereqResolverKey : visited.getPrereqResolvers()) {
324 Visited prereqVisited = visitedByKey.get(prereqResolverKey);
325 assembleLinearResolutionPlan(prereqVisited, visitedByKey, plan);
326 plan.add(prereqResolverKey);
327 }
328 }
329
330
331
332
333
334 private TermResolver<? extends Object> createDestination(final String termName) {
335 TermResolver<?> destination = new TermResolver<Object>() {
336 final String dest = "termResolutionEngineDestination";
337 @Override
338 public int getCost() {
339 return 0;
340 }
341 @Override
342 public String getOutput() {
343 return dest;
344 }
345 @Override
346 public Set<String> getPrerequisites() {
347 return Collections.<String>singleton(termName);
348 }
349 @Override
350 public Set<String> getParameterNames() {
351 return Collections.emptySet();
352 }
353 @Override
354 public Object resolve(Map<String, Object> resolvedPrereqs, Map<String, String> parameters) throws TermResolutionException {
355 return null;
356 }
357 };
358 return destination;
359 }
360
361
362 private static class ToVisit implements Comparable<ToVisit> {
363
364 private final int precost;
365 private final int addcost;
366 private final TermResolverKey resolverKey;
367
368
369 private final TermResolverKey parentKey;
370
371
372
373
374
375 public ToVisit(int precost, TermResolver resolver, TermResolver parent) {
376 super();
377 this.precost = precost;
378 this.addcost = resolver.getCost();
379 this.resolverKey = new TermResolverKey(resolver);
380
381 if (parent != null) {
382 this.parentKey = new TermResolverKey(parent);
383 } else {
384 this.parentKey = null;
385 }
386 }
387
388 public int getCost() {
389 return precost + addcost;
390 }
391
392 @Override
393 public boolean equals(Object obj) {
394 if (this == obj) return true;
395 if (obj == null) return false;
396 if (getClass() != obj.getClass())
397 return false;
398 ToVisit other = (ToVisit)obj;
399 return this.compareTo(other) == 0;
400 }
401
402
403
404
405 @Override
406 public int hashCode() {
407 final int prime = 31;
408 int result = 1;
409 result = prime * result + getCost();
410 result = prime * result + ((resolverKey == null) ? 0 : resolverKey.hashCode());
411 return result;
412 }
413
414
415
416
417 @Override
418 public int compareTo(ToVisit o) {
419 if (o == null) return 1;
420 if (getCost() > o.getCost()) return 1;
421 if (getCost() < o.getCost()) return -1;
422 return resolverKey.compareTo(o.resolverKey);
423 }
424
425 public TermResolverKey getTermResolverKey() {
426 return resolverKey;
427 }
428
429 public TermResolverKey getParentKey() {
430 return parentKey;
431 }
432
433 @Override
434 public String toString() {
435 return getClass().getSimpleName()+"("+getTermResolverKey()+")";
436 }
437 }
438
439 protected static class TermResolverKey implements Comparable<TermResolverKey> {
440 private final List<String> data;
441 private final String [] params;
442
443
444 private static final String[] TERM_SPEC_TYPER = new String[0];
445 private static final String[] STRING_TYPER = new String[0];
446
447 public TermResolverKey(TermResolver resolver) {
448 this(resolver.getOutput(), resolver.getParameterNames(), resolver.getPrerequisites());
449 }
450
451 private TermResolverKey(String dest, Set<String> paramSet, Set<String> prereqs) {
452 if (dest == null) throw new IllegalArgumentException("dest parameter must not be null");
453 data = new ArrayList<String>(1 + ((prereqs == null) ? 0 : prereqs.size()));
454 data.add(dest);
455 if (!CollectionUtils.isEmpty(paramSet)) {
456 params = paramSet.toArray(STRING_TYPER);
457 Arrays.sort(params);
458 } else {
459 params = STRING_TYPER;
460 }
461 if (prereqs != null) {
462
463 String [] prereqsArray = prereqs.toArray(TERM_SPEC_TYPER);
464 Arrays.sort(prereqsArray);
465 for (String prereq : prereqsArray) {
466 data.add(prereq);
467 }
468 }
469 }
470
471 public String getOutput() {
472 return data.get(0);
473 }
474
475 @Override
476 public int compareTo(TermResolverKey o) {
477 if (o == null) return 1;
478
479 Iterator<String> mDataIter = data.iterator();
480 Iterator<String> oDataIter = o.data.iterator();
481
482 while (mDataIter.hasNext() && oDataIter.hasNext()) {
483 int itemCompareResult = mDataIter.next().compareTo(oDataIter.next());
484 if (itemCompareResult != 0) return itemCompareResult;
485 }
486
487 if (mDataIter.hasNext()) return 1;
488 if (oDataIter.hasNext()) return -1;
489
490 return 0;
491 }
492
493
494
495
496 @Override
497 public int hashCode() {
498 return data.hashCode();
499 }
500
501
502
503
504 @Override
505 public boolean equals(Object obj) {
506 if (this == obj)
507 return true;
508 if (obj == null)
509 return false;
510 if (getClass() != obj.getClass())
511 return false;
512 TermResolverKey other = (TermResolverKey) obj;
513 return this.compareTo(other) == 0;
514 }
515
516 @Override
517 public String toString() {
518 StringBuilder sb = new StringBuilder();
519 sb.append(getClass().getSimpleName());
520 sb.append("(");
521 Iterator<String> iter = data.iterator();
522 sb.append(iter.next().toString());
523 if (params.length != 0) {
524 sb.append("+");
525 ArrayUtils.toString(params);
526 }
527
528 if (iter.hasNext()) sb.append(" <- ");
529 boolean first = true;
530 while (iter.hasNext()) {
531 if (first) first = false;
532 else sb.append(", ");
533 sb.append(iter.next().toString());
534 }
535
536
537 return sb.toString();
538 }
539 }
540
541
542 private static class Visited {
543 private TermResolverKey resolverKey;
544 private List<TermResolverKey> pathTo;
545 private Set<String> remainingPrereqs;
546 private Map<String, TermResolverKey> prereqResolvers;
547 private int cost;
548
549
550
551
552
553
554
555 public Visited(TermResolverKey resolverKey, List<TermResolverKey> pathTo, Set<String> prereqs, int cost) {
556 super();
557 this.resolverKey = resolverKey;
558 this.pathTo = pathTo;
559 this.remainingPrereqs = new HashSet<String>(prereqs);
560 this.prereqResolvers = new HashMap<String, TermResolverKey>();
561 this.cost = cost;
562 }
563
564
565
566
567 public List<TermResolverKey> getPathTo() {
568 return pathTo;
569 }
570
571 public TermResolverKey getResolverKey() {
572 return resolverKey;
573 }
574
575 public Collection<TermResolverKey> getPrereqResolvers() {
576 return prereqResolvers.values();
577 }
578
579
580
581
582 public boolean isFullyPlanned() {
583 return remainingPrereqs.isEmpty();
584 }
585
586 public int getCost() {
587 return cost;
588 }
589
590 public void addPlannedPrereq(TermResolverKey termResolverKey) {
591 remainingPrereqs.remove(termResolverKey.getOutput());
592 prereqResolvers.put(termResolverKey.getOutput(), termResolverKey);
593 }
594
595 public void addPlannedPrereq(String termName) {
596 remainingPrereqs.remove(termName);
597 }
598 }
599
600 private static class InvalidResolutionPathException extends Exception {
601 private static final long serialVersionUID = 1L;
602
603 public InvalidResolutionPathException() {
604 }
605 }
606
607 }
608
609