View Javadoc

1   /**
2    * Copyright 2005-2012 The Kuali Foundation
3    *
4    * Licensed under the Educational Community License, Version 2.0 (the "License");
5    * you may not use this file except in compliance with the License.
6    * You may obtain a copy of the License at
7    *
8    * http://www.opensource.org/licenses/ecl2.php
9    *
10   * Unless required by applicable law or agreed to in writing, software
11   * distributed under the License is distributed on an "AS IS" BASIS,
12   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13   * See the License for the specific language governing permissions and
14   * limitations under the License.
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  	// should this use soft refs?  Will require some refactoring to check if the referenced object is around;
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  		// build plan w/ termName spec for correct TermResolver selection
77  		List<TermResolverKey> resolutionPlan = buildTermResolutionPlan(termName);
78  		// TODO: could cache these plans somewhere, since future agendas will likely require the same plans
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  				// build prereqs
88  				Map<String, Object> resolvedPrereqs = new HashMap<String, Object>();
89  				
90  				// The plan order should guarantee these prereqs exist and are cached.
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  				// The destination Term (and only the dest Term) can be parameterized
98  				if (termName.equals(resolver.getOutput())) {
99  					providedParameters = term.getParameters();
100 					
101 					// throw a TermResolutionException if the params doen't match up
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 						// Shouldn't happen due to checks in buildResolutionPlan
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 	 * This method checks that the required parameters (as returned by the {@link TermResolver} via 
124 	 * {@link TermResolver#getParameterNames()}) are met in the {@link Map} of provided parameters.
125 	 * 
126 	 * @param resolver
127 	 * @param providedParameters
128 	 * @throws TermResolutionException
129 	 */
130 	private void validateTermParameters(TermResolver<?> resolver,
131 			Map<String, String> providedParameters)
132 			throws TermResolutionException {
133 		// validate that params match what the TermResolver requires
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 		// our result
157 		List<TermResolverKey> resolutionPlan = null;
158 
159 		// Holds the resolvers we've visited, along with the needed metadata for generating our final plan
160 		Map<TermResolverKey, Visited> visitedByKey = new HashMap<TermResolverKey, Visited>();
161 
162 		// this holds a least cost first list of nodes remaining to be explored
163 		PriorityQueue<ToVisit> toVisits = new PriorityQueue<ToVisit>(); // nice grammar there cowboy
164 
165 		// dummy resolver to be the root of this tree
166 		// Do I really need this?  Yes, because there may be more than one resolver that resolves to the desired termName,
167 		// so this destination unifies the trees of those candidate resolvers
168 		TermResolver destination = createDestination(termName); // problem is we can't get this one out of the registry
169 		TermResolverKey destinationKey = new TermResolverKey(destination);
170 
171 		LOG.debug("Beginning resolution tree search for " + termName);
172 
173 		// seed our queue of resolvers to visit
174 		// need to be aware of null parent for root ToVisit
175 		toVisits.add(new ToVisit(0, destination, null));
176 
177 		// there may not be a viable plan
178 		boolean plannedToDestination = false;
179 
180 		// We'll do a modified Dijkstra's shortest path algorithm, where at each leaf we see if we've planned out
181 		// termName resolution all the way up to the root, our destination.  If so, we just reconstruct our plan.
182 		while (!plannedToDestination && toVisits.size() > 0) {
183 			// visit least cost node remaining
184 			ToVisit visiting = toVisits.poll();
185 
186 			LOG.debug("visiting " + visiting.getTermResolverKey());
187 
188 			// the resolver is the edge in our tree -- we don't get it directly from the termResolversByKey Map, because it could be our destination
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; // We've already visited this one
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 			// keep track of any prereqs that we already have handy
201 			List<String> metPrereqs = new LinkedList<String>();
202 
203 			// see what prereqs we have already, and which we'll need to visit
204 			if (prereqs != null) for (String prereq : prereqs) {
205 				if (!termCache.containsKey(new Term(prereq, null))) {
206 					// enqueue all resolvers in toVisits
207 					List<TermResolver<?>> prereqResolvers = termResolversByOutput.get(prereq);
208 					if (prereqResolvers != null) for (TermResolver prereqResolver : prereqResolvers) {
209 						// Only TermResolvers that don't take paramaterized terms can be chained, so:
210 						// if the TermResolver doesn't take parameters, or it resolves the output termName
211 						if (CollectionUtils.isEmpty(prereqResolver.getParameterNames()) ||
212 								termName.equals(prereqResolver.getOutput()))
213 						{
214 							// queue it up for visiting
215 							toVisits.add(new ToVisit(visiting.getCost() /* cost to get to this resolver */, prereqResolver, resolver));
216 						}
217 					}
218 				} else {
219 					metPrereqs.add(prereq);
220 				}
221 			}
222 
223 			// Build visited info
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 			// build result from Visited tree.
232 			resolutionPlan = new LinkedList<TermResolverKey>();
233 
234 			assembleLinearResolutionPlan(visitedByKey.get(destinationKey), visitedByKey, resolutionPlan);
235 		}
236 		return resolutionPlan;
237 	}
238 
239 	/**
240 	 *  @return the Visited object for the resolver we just, er, well, visited.
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 	 * our dummy destination isn't allowed to pollute termResolversByKey, hence the ugly conditional encapsulated herein
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 	 * @param visited
272 	 * @param destinationKey
273 	 * @param visitedByKey
274 	 * @return
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 			// no traversing further yet, instead we need to check how far up the tree is fully planned out
283 			// step backwards toward the root of the tree and see if we're good all the way to our objective.
284 			// if a node fully planned, move up the tree (towards the root) to see if its parent is fully planned, and so on.
285 
286 			if (visited.getPathTo().size() > 0) {
287 				// reverse the path to
288 				List<TermResolverKey> reversePathTo = new ArrayList<TermResolverKey>(visited.getPathTo());
289 				Collections.reverse(reversePathTo);
290 
291 				// we use this to propagate resolutions up the tree
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 						// Woot! Job's done!
303 						plannedToDestination = true;
304 						break;
305 					} else if (!ancestorVisited.isFullyPlanned()) { // if the ancestor isn't fully planned, we're done
306 						LOG.debug("Still have planning to do.");
307 						break;
308 					}
309 					// update previousAncestor reference for next iteration
310 					previousAncestor = ancestorVisited;
311 				}
312 			} else {
313 				// we're done already! do a jig?
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 		// DFS
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 	 * Create our dummy destination resolver
332 	 * @param termName
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 		// the parent key is not being used for comparison purposes currently
369 		private final TermResolverKey parentKey;
370 
371 		/**
372 		 * @param precost
373 		 * @param resolver
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 		/* (non-Javadoc)
403 		 * @see java.lang.Object#hashCode()
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 		 * {@inheritDoc Comparable}
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 		// just used for toArray call
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())); // size ArrayList perfectly
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; // just assigning a handy zero length String array
460 			}
461 			if (prereqs != null) {
462 				// this is painful, but to be able to compare we need a defined order
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 		/* (non-Javadoc)
494 		 * @see java.lang.Object#hashCode()
495 		 */
496 		@Override
497 		public int hashCode() {
498 			return data.hashCode();
499 		}
500 
501 		/* (non-Javadoc)
502 		 * @see java.lang.Object#equals(java.lang.Object)
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 		 * @param resolverKey
551 		 * @param pathTo
552 		 * @param prereqs
553 		 * @param cost
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 		 * @return the path from the goal node down to this resolver
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 		 * @return true if resolution of all the prerequisites has been planned
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