View Javadoc

1   package cz.cuni.amis.pogamut.sposh.elements;
2   
3   import java.text.CharacterIterator;
4   import java.text.StringCharacterIterator;
5   import java.util.ArrayList;
6   import java.util.Arrays;
7   import java.util.Collection;
8   import java.util.Iterator;
9   import java.util.LinkedList;
10  import java.util.List;
11  
12  /**
13   * {@link LapPath} is used to describe path from the root of {@link PoshPlan} to
14   * some subnode of the plan. The path consists from several links, each link
15   * describes which cild of current node should be used to change into new
16   * current node.
17   *
18   * There are three major features we require:
19   *
20   * 1. Parse path from string
21   *
22   * 2. traverse plan according to the path and return the node.
23   *
24   * 3. serialize path to the string
25   *
26   * Examples of serialized path:
27   *
28   * /P:0/DC:0 - default plan, drive collection 0
29   *
30   * /P:0/DC:0/S:1 - Drive collection 0, goal sense 1
31   *
32   * /P:0/DC:0/DE:4 - fifth drive in the DC
33   *
34   * /P:0/DC:0/DE:4/S:0 - first trigger sense of fifth drive in the DC
35   *
36   * /P:0/DC:0/DE:4/A:0/AP:4 - reference to AP node.
37   *
38   * @author Honza
39   */
40  public final class LapPath implements Iterable<LapPath.Link> {
41  
42      /**
43       * Empty path with no links.
44       */
45      public static final LapPath EMPTY = new LapPath();
46      /**
47       * Path to the root of the plan, equivalent of <tt>/P:0</tt>
48       */
49      public static LapPath PLAN_PATH = new LapPath().concat(LapType.PLAN, 0);
50      /**
51       * Path to the {@link LapType#DRIVE_COLLECTION}. Equivalent of
52       * <tt>/P:0/DC:0</tt>
53       */
54      public static LapPath DRIVE_COLLECTION_PATH = new LapPath().concat(LapType.PLAN, 0).concat(LapType.DRIVE_COLLECTION, 0);
55      /**
56       * Character used to separate links.
57       */
58      private static char LINK_SEPARATOR = '/';
59      /**
60       * Character used to separate type from id in the {@link Link}.
61       */
62      private static char TYPE_SEPARATOR = ':';
63      /**
64       * All links of the path. Each link determines next current node.
65       */
66      private final Link[] links;
67  
68      public LapPath() {
69          this.links = new Link[0];
70      }
71  
72      LapPath(List<Link> links) {
73          this.links = links.toArray(new Link[links.size()]);
74      }
75  
76      /**
77       * Make sure that link from {@link Iterator#next() } is {@link LapType#PLAN}
78       * with id 0.
79       */
80      private void traverseRoot(Iterator<Link> iter) {
81          if (!iter.hasNext()) {
82              throw new IllegalStateException("No plan specified in the path.");
83          }
84          Link rootLink = iter.next();
85          if (rootLink.type != LapType.PLAN) {
86              throw new IllegalStateException("First link always has type " + LapType.PLAN + ", but is " + rootLink.type);
87          }
88          if (rootLink.id != 0) {
89              throw new IndexOutOfBoundsException("Id of plan must be 0, but is " + rootLink.id);
90          }
91      }
92  
93      /**
94       * Make sure that link from {@link Iterator#next() } is {@link LapType#DRIVE_COLLECTION}
95       * with id 0.
96       */
97      private void traversePlan(Iterator<Link> iter) {
98          Link planLink = iter.next();
99          if (planLink.type != LapType.DRIVE_COLLECTION) {
100             throw new IllegalStateException("Second link always has type " + LapType.DRIVE_COLLECTION + ", but is " + planLink.type);
101         }
102         if (planLink.id != 0) {
103             throw new IndexOutOfBoundsException("Id of drive collection must be 0, but is " + planLink.id);
104         }
105     }
106 
107     /**
108      * Traverse path from {@link DriveCollection} downwards. Use link from {@link Iterator#next()
109      * }.
110      */
111     private static PoshElement traverseDriveCollection(Iterator<Link> iter, PoshPlan plan) {
112         Link driveCollectionLink = iter.next();
113         DriveCollection driveCollection = plan.getDriveCollection();
114 
115         if (driveCollectionLink.type == LapType.DRIVE_ELEMENT) {
116             DriveElement drive = driveCollection.getDrives().get(driveCollectionLink.id);
117             if (!iter.hasNext()) {
118                 return drive;
119             }
120             return traverseDrive(iter, plan, drive);
121         } else if (driveCollectionLink.type == LapType.SENSE) {
122             Sense goalSense = driveCollection.getGoal().get(driveCollectionLink.id);
123             if (iter.hasNext()) {
124                 throw new IllegalStateException("Goal sense must be last link, but isn't.");
125             }
126             return goalSense;
127         } else {
128             throw new IllegalStateException("Link was expected to be DRIVE or SENSE, but is " + driveCollectionLink.type);
129         }
130     }
131 
132     /**
133      * Traverse path from drive downwards. Use link from {@link Iterator#next()
134      * }.
135      */
136     private static PoshElement traverseDrive(Iterator<Link> iter, PoshPlan plan, DriveElement drive) {
137         Link driveLink = iter.next();
138         // now we are in drive, either action or 
139         if (driveLink.type == LapType.SENSE) {
140             Sense driveTriggerSense = drive.getTrigger().get(driveLink.id);
141             if (iter.hasNext()) {
142                 throw new IllegalStateException("Drive trigger sense must be last link, but isn't.");
143             }
144             return driveTriggerSense;
145         } else if (driveLink.type == LapType.ACTION) {
146             if (driveLink.id != 0) {
147                 throw new IndexOutOfBoundsException("Id of action in drive should be 0, but is " + driveLink.id);
148             }
149             TriggeredAction action = drive.getAction();
150             if (!iter.hasNext()) {
151                 return action;
152             }
153             return traverseReference(iter, plan, action);
154         } else {
155             throw new IllegalStateException("Link was expected to be SENSE or ACTION, but is " + driveLink.type);
156         }
157     }
158 
159     /**
160      * Traverse reference {@link TriggeredAction}. Use link from {@link Iterator#next()
161      * }. This can be used only if {@link TriggeredAction} actually references
162      * something.
163      */
164     private static PoshElement traverseReference(Iterator<Link> iter, PoshPlan plan, TriggeredAction reference) {
165         Link referenceLink = iter.next();
166         if (referenceLink.type == LapType.ADOPT) {
167             Adopt adopt = plan.getAdopts().get(referenceLink.id);
168             if (!reference.getName().equals(adopt.getName())) {
169                 throw new IllegalStateException("Reference link with name " + reference.getName() + " does not match referencing Adopt " + referenceLink.id + " with name " + adopt.getName());
170             }
171             if (!iter.hasNext()) {
172                 return adopt;
173             }
174             return traverseAdopt(iter, plan, adopt);
175         } else if (referenceLink.type == LapType.ACTION_PATTERN) {
176             ActionPattern actionPattern = plan.getActionPatterns().get(referenceLink.id);
177             if (!reference.getName().equals(actionPattern.getName())) {
178                 throw new IllegalStateException("Reference link with name " + reference.getName() + " does not match referencing AP " + referenceLink.id + " with name " + actionPattern.getName());
179             }
180             if (!iter.hasNext()) {
181                 return actionPattern;
182             }
183             return traverseActionPattern(iter, plan, actionPattern);
184         } else if (referenceLink.type == LapType.COMPETENCE) {
185             Competence competence = plan.getCompetences().get(referenceLink.id);
186             if (!reference.getName().equals(competence.getName())) {
187                 throw new IllegalStateException("Reference link with name " + reference.getName() + " does not match referencing C " + referenceLink.id + " with name " + competence.getName());
188             }
189             if (!iter.hasNext()) {
190                 return competence;
191             }
192             return traverseCompetence(iter, plan, competence);
193         } else {
194             throw new IllegalStateException("Link was expected to be ADOPT, ACTION_PATTERN or COMPETENCE, but is " + referenceLink.type);
195         }
196     }
197 
198     /**
199      * Traverse from adopt node to next item. Use link from {@link Iterator#next()
200      * }, it is guaranteed to exist.
201      */
202     private static PoshElement traverseAdopt(Iterator<Link> iter, PoshPlan plan, Adopt adopt) {
203         Link adoptLink = iter.next();
204         if (adoptLink.type == LapType.SENSE) {
205             Sense exitConditionSense = adopt.getExitCondition().get(adoptLink.id);
206             if (iter.hasNext()) {
207                 throw new IllegalStateException("Exit condition sense of adopt is not last link in path.");
208             }
209             return exitConditionSense;
210         } else if (adoptLink.type == LapType.ACTION) {
211             if (adoptLink.id != 0) {
212                 throw new IllegalStateException("Id of action in adopt should be 0, but is " + adoptLink.id);
213             }
214             TriggeredAction action = adopt.getAdoptedElement();
215             if (!iter.hasNext()) {
216                 return action;
217             }
218             return traverseReference(iter, plan, action);
219         } else {
220             throw new IllegalStateException("Expecting sense or action in adopt, got " + adoptLink.type);
221         }
222     }
223 
224     /**
225      * Traverse from action pattern to next item. Use link from {@link Iterator#next()
226      * }.
227      */
228     private static PoshElement traverseActionPattern(Iterator<Link> iter, PoshPlan plan, ActionPattern actionPattern) {
229         Link actionPatternLink = iter.next();
230         if (actionPatternLink.type != LapType.ACTION) {
231             throw new IllegalStateException("Action pattern can have only action subnodes.");
232         }
233 
234         TriggeredAction action = actionPattern.getActions().get(actionPatternLink.id);
235         if (!iter.hasNext()) {
236             return action;
237         }
238         return traverseReference(iter, plan, action);
239     }
240 
241     /**
242      * Traverse from choice to next node. Use link from {@link Iterator#next()
243      * }.
244      */
245     private static PoshElement traverseCompetence(Iterator<Link> iter, PoshPlan plan, Competence competence) {
246         Link competenceLink = iter.next();
247         if (competenceLink.type != LapType.COMPETENCE_ELEMENT) {
248             throw new IllegalStateException("Link from competence can be only choice, but is " + competenceLink.type);
249         }
250         CompetenceElement choice = competence.getChildDataNodes().get(competenceLink.id);
251         if (!iter.hasNext()) {
252             return choice;
253         }
254         return traverseChoice(iter, plan, choice);
255     }
256 
257     /**
258      * Traverse from choice to next item. Use link from {@link Iterator#next()
259      * }.
260      */
261     private static PoshElement traverseChoice(Iterator<Link> iter, PoshPlan plan, CompetenceElement choice) {
262         Link choiceLink = iter.next();
263         if (choiceLink.type == LapType.SENSE) {
264             Sense choiceTriggerSense = choice.getTrigger().get(choiceLink.id);
265             if (iter.hasNext()) {
266                 throw new IllegalStateException("Trigger sense of choice is not last link in path.");
267             }
268             return choiceTriggerSense;
269         } else if (choiceLink.type == LapType.ACTION) {
270             if (choiceLink.id != 0) {
271                 throw new IllegalStateException("Id of action in choice should be 0, but is " + choiceLink.id);
272             }
273             TriggeredAction action = choice.getAction();
274             if (!iter.hasNext()) {
275                 return action;
276             }
277             return traverseReference(iter, plan, action);
278         } else {
279             throw new IllegalStateException("Expecting sense or action in choice, got " + choiceLink.type);
280         }
281     }
282 
283     /**
284      * Methods with name traverseXYZ mean traverse from XYZ below xyzLink means
285      * gor from xyz below according to type and id.
286      *
287      * @param plan
288      * @return Found node
289      * @throws IllegalStateException If path does not play nice with tree, e.g.
290      * when paths wants sense subnode in AP, but there is none according to
291      * syntax
292      * @throws IndexOutOfBoundsException If some id on the path references to
293      * nonexistent node.
294      */
295     public <T extends PoshElement> T traversePath(PoshPlan plan) {
296         Iterator<Link> iterator = Arrays.asList(links).iterator();
297 
298         traverseRoot(iterator);
299         if (!iterator.hasNext()) {
300             return (T) plan;
301         }
302 
303         traversePlan(iterator);
304         if (!iterator.hasNext()) {
305             return (T) plan.getDriveCollection();
306         }
307 
308         return (T) traverseDriveCollection(iterator, plan);
309     }
310 
311     /**
312      * Construct path from the @endElement to its very first branch node(DC, AD,
313      * AP, C) upward in the tree. E.g. if @endElement is third trigger {@link LapType#SENSE}
314      * in the second {@link CompetenceElement} of fourth {@link Competence}, the
315      * path will be something like <tt>/C:4/CE:2/S:3</tt>, since the {@link Competence}
316      * is a branch node.
317      *
318      * @param endElement Element whose ancestor we use to create path.
319      * @return Path from closest ancestor branch node of element to @endElement
320      */
321     public static LapPath getLinkPath(PoshElement endElement) {
322         LapPath path = LapPath.EMPTY;
323         PoshElement element = endElement;
324         PoshPlan plan = endElement.getRootNode();
325         while (element != plan) {
326             path = new LapPath().concat(element.getType(), element.getId()).concat(path);
327             element = element.getParent();
328         }
329         return path;
330     }
331 
332     /**
333      * Get string from @iter until end of string or until terminator is
334      * encountered.
335      *
336      * @param iter Character iterator.
337      */
338     private static String getStringUntil(CharacterIterator iter, char terminator) {
339         StringBuilder sb = new StringBuilder();
340         for (char currentChar = iter.current();
341                 currentChar != CharacterIterator.DONE && currentChar != terminator;
342                 currentChar = iter.next()) {
343             sb.append(currentChar);
344         }
345         return sb.toString();
346     }
347 
348     private static String getDecimals(CharacterIterator iter) {
349         StringBuilder sb = new StringBuilder();
350         for (char currentChar = iter.current();
351                 currentChar >= '0' && currentChar <= '9';
352                 currentChar = iter.next()) {
353             sb.append(currentChar);
354         }
355         return sb.toString();
356     }
357 
358     /**
359      * Take the @iter and get the link from it. Link consists from {@link #LINK_SEPARATOR}, {@link LapType}, {@link #TYPE_SEPARATOR}
360      * and integer id.
361      *
362      * @param iter Iterator from which to get characters.
363      * @return
364      */
365     private static Link parseLink(CharacterIterator iter) throws ParseException {
366 
367         char linkSeparator = iter.current();
368         if (linkSeparator == CharacterIterator.DONE || linkSeparator != LapPath.LINK_SEPARATOR) {
369             throw new ParseException("Expected " + LapPath.LINK_SEPARATOR + " at " + iter.getIndex());
370         }
371         iter.next();
372         String typeString = getStringUntil(iter, LapPath.TYPE_SEPARATOR);
373 
374         LapType linkType = null;
375         for (LapType type : LapType.values()) {
376             if (type.getName().equals(typeString)) {
377                 linkType = type;
378             }
379         }
380         if (linkType == null) {
381             throw new ParseException("No LapType '" + typeString + "' exists.");
382         }
383 
384 
385         char typeSeparatorChar = iter.current();
386         if (typeSeparatorChar == CharacterIterator.DONE || typeSeparatorChar != LapPath.TYPE_SEPARATOR) {
387             throw new ParseException("Expected " + LapPath.TYPE_SEPARATOR + " at " + iter.getIndex());
388         }
389         iter.next();
390 
391         try {
392             String idString = getDecimals(iter);
393             int linkId = Integer.parseInt(idString);
394 
395             return new Link(linkType, linkId);
396         } catch (NumberFormatException ex) {
397             throw new ParseException(ex.getMessage());
398         }
399     }
400 
401     /**
402      * Parse @serializedPath to
403      *
404      * @param serializedPath {@link LapPath} in serialized form, e.g.
405      * /P:0/DC:0/DE:1/S:1
406      * @return Path object created according to @serializedPath
407      */
408     public static LapPath parse(String serializedPath) throws ParseException {
409         CharacterIterator iter = new StringCharacterIterator(serializedPath);
410 
411         List<Link> parsedLinks = new LinkedList<Link>();
412         do {
413             parsedLinks.add(parseLink(iter));
414         } while (iter.current() != CharacterIterator.DONE);
415         return new LapPath(parsedLinks);
416     }
417 
418     /**
419      * Create and return new LapPath by appending new link to all links of
420      * current path. This method does not change this path (LapPath is
421      * immutable)/
422      *
423      */
424     public LapPath concat(LapType type, int id) {
425         List<Link> newPathLinks = new LinkedList<Link>(Arrays.asList(links));
426         newPathLinks.add(new Link(type, id));
427 
428         return new LapPath(newPathLinks);
429     }
430 
431     @Override
432     public String toString() {
433         StringBuilder sb = new StringBuilder();
434         for (Link link : links) {
435             sb.append(link.toString());
436         }
437         return sb.toString();
438     }
439 
440     @Override
441     public boolean equals(Object obj) {
442         if (obj == null) {
443             return false;
444         }
445         if (getClass() != obj.getClass()) {
446             return false;
447         }
448         final LapPath other = (LapPath) obj;
449         if (!Arrays.deepEquals(this.links, other.links)) {
450             return false;
451         }
452         return true;
453     }
454 
455     @Override
456     public int hashCode() {
457         int hash = 5;
458         hash = 59 * hash + Arrays.deepHashCode(this.links);
459         return hash;
460     }
461 
462     /**
463      * Create new path consisting from this path and @appendPath
464      *
465      * @param appendPath Path that will be appended to this and returned.
466      */
467     public LapPath concat(LapPath appendPath) {
468         List<Link> concatLinks = new LinkedList<Link>(Arrays.asList(this.links));
469         concatLinks.addAll(Arrays.asList(appendPath.links));
470 
471         LapPath concatPath = new LapPath(concatLinks);
472         return concatPath;
473     }
474 
475     /**
476      * @return new path from all links of this this path + appendedLink.
477      */
478     public LapPath concat(Link appendedLink) {
479         List<Link> concatLinks = new LinkedList<Link>(Arrays.asList(this.links));
480         concatLinks.add(appendedLink);
481 
482         LapPath concatPath = new LapPath(concatLinks);
483         return concatPath;
484     }
485 
486     /**
487      * Return subpath of this path.
488      *
489      * @param beginIndex The beginning link index, inclusive.
490      * @param endIndex The ending link index, exclusive.
491      * @return subpath.
492      * @throws IndexOutOfBoundsException If beginIndex &lt; 0 or endIndex &gt;
493      * length of path or beginIndex &gt; endIndex.
494      */
495     public LapPath subpath(int beginIndex, int endIndex) {
496         if (beginIndex < 0) {
497             throw new IndexOutOfBoundsException("beginIndex(" + beginIndex + ") is negative.");
498         }
499         if (endIndex > links.length) {
500             throw new IndexOutOfBoundsException("endIndex(" + endIndex + ") is greater than length of path(" + links.length + ").");
501         }
502         if (beginIndex > endIndex) {
503             throw new IndexOutOfBoundsException("beginIndex(" + beginIndex + ") is greater than endIndex(" + endIndex + ").");
504         }
505         Link[] subarray = Arrays.copyOfRange(links, beginIndex, endIndex);
506         return new LapPath(Arrays.asList(subarray));
507     }
508 
509     /**
510      * @return Number of links in the path.
511      */
512     public int length() {
513         return links.length;
514     }
515 
516     @Override
517     public Iterator<Link> iterator() {
518         return Arrays.asList(links).iterator();
519     }
520 
521     public Link getLink(int linkId) {
522         return links[linkId];
523     }
524 
525     /**
526      * Get index of first link equal to the @link.
527      *
528      * @param searchedLink link to search for
529      * @return Index of first occurance of @link in links of the path. -1 If not found.
530      */
531     int getLinkIndex(Link searchedLink) {
532         for (int index = 0; index < links.length; ++index) {
533             if (links[index] == searchedLink) {
534                 return index;
535             }
536         }
537         throw new IllegalArgumentException("Unable to find the link.");
538     }
539 
540     /**
541      * One link of the path, immutable.
542      */
543     public static class Link {
544 
545         /**
546          * Type of this link
547          */
548         private final LapType type;
549         /**
550          * Id of this link. Id means that we take all children of current node
551          * that have correct {@link #type} and pick the {@link #id}th one.
552          */
553         private final int id;
554 
555         public Link(LapType type, int id) {
556             this.type = type;
557             this.id = id;
558         }
559 
560         /**
561          * @return Type of path link
562          */
563         public LapType getType() {
564             return type;
565         }
566 
567         /**
568          * @return Id of link. Id is index into list of all children of parent
569          * with same {@link #getType() }.
570          */
571         public int getId() {
572             return id;
573         }
574 
575         @Override
576         public String toString() {
577             StringBuilder sb = new StringBuilder();
578 
579             sb.append(LINK_SEPARATOR);
580             sb.append(type.getName());
581             sb.append(TYPE_SEPARATOR);
582             sb.append(id);
583 
584             return sb.toString();
585         }
586 
587         @Override
588         public boolean equals(Object obj) {
589             if (obj == null) {
590                 return false;
591             }
592             if (getClass() != obj.getClass()) {
593                 return false;
594             }
595             final Link other = (Link) obj;
596             if (this.type != other.type) {
597                 return false;
598             }
599             if (this.id != other.id) {
600                 return false;
601             }
602             return true;
603         }
604 
605         @Override
606         public int hashCode() {
607             int hash = 3;
608             hash = 37 * hash + (this.type != null ? this.type.hashCode() : 0);
609             hash = 37 * hash + this.id;
610             return hash;
611         }
612     }
613 }