View Javadoc

1   /**
2    * Copyright 2005-2013 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  /**
41   * An implementation of {@link TermResolutionEngine}
42   * @author Kuali Rice Team (rice.collab@kuali.org)
43   */
44  public class TermResolutionEngineImpl implements TermResolutionEngine {
45  	private static final org.apache.log4j.Logger LOG = org.apache.log4j.Logger.getLogger(TermResolutionEngineImpl.class);
46  	
47  	private final Map<String, List<TermResolver<?>>> termResolversByOutput = new HashMap<String, List<TermResolver<?>>>();
48  	private final Map<TermResolverKey, TermResolver<?>> termResolversByKey = new HashMap<TermResolverKey, TermResolver<?>>(); 
49  	
50  	// should this use soft refs?  Will require some refactoring to check if the referenced object is around;
51  	private final Map<Term, Object> termCache = new HashMap<Term, Object>();
52  
53  	@Override
54  	public void addTermValue(Term term, Object value) {
55  		termCache.put(term, value);
56  	}
57  	
58  	@Override
59  	public void addTermResolver(TermResolver<?> termResolver) {
60  		if (termResolver == null) throw new IllegalArgumentException("termResolver is reuqired");
61  		if (termResolver.getOutput() == null) throw new IllegalArgumentException("termResolver.getOutput() must not be null");
62  
63  		List<TermResolver<?>> termResolvers = termResolversByOutput.get(termResolver.getOutput());
64  		if (termResolvers == null) {
65  			termResolvers = new LinkedList<TermResolver<?>>();
66  			termResolversByOutput.put(termResolver.getOutput(), termResolvers);
67  		}
68  		termResolversByKey.put(new TermResolverKey(termResolver), termResolver);
69  		termResolvers.add(termResolver);
70  	}
71  
72  	@SuppressWarnings("unchecked")
73  	@Override
74  	public <T> T resolveTerm(Term term) throws TermResolutionException {
75  		LOG.debug("+--> resolveTerm(" + term + ")");
76  		if (termCache.containsKey(term)) return (T)termCache.get(term);
77  		
78  		String termName = term.getName();
79  		
80  		// build plan w/ termName spec for correct TermResolver selection
81  		List<TermResolverKey> resolutionPlan = buildTermResolutionPlan(termName);
82  		// TODO: could cache these plans somewhere, since future agendas will likely require the same plans
83  		
84  		LOG.debug("resolutionPlan: " + (resolutionPlan == null ? "null" : StringUtils.join(resolutionPlan.iterator(), ", ")));
85  		
86  		if (resolutionPlan != null) {
87  			LOG.debug("executing plan");
88  			for (TermResolverKey resolverKey : resolutionPlan) {
89  				TermResolver<?> resolver = termResolversByKey.get(resolverKey);
90  				
91  				// build prereqs
92  				Map<String, Object> resolvedPrereqs = new HashMap<String, Object>();
93  				
94  				// The plan order should guarantee these prereqs exist and are cached.
95  				for (String prereq : resolver.getPrerequisites()) {
96  					Object resolvedPrereq = termCache.get(new Term(prereq, null));
97  					resolvedPrereqs.put(prereq, resolvedPrereq);
98  				}
99  				
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