View Javadoc
1   package emissary.core;
2   
3   import emissary.directory.KeyManipulator;
4   import emissary.place.IServiceProviderPlace;
5   
6   import jakarta.annotation.Nullable;
7   import org.apache.commons.collections4.CollectionUtils;
8   import org.apache.commons.lang3.StringUtils;
9   
10  import java.io.Serializable;
11  import java.util.ArrayDeque;
12  import java.util.ArrayList;
13  import java.util.Collections;
14  import java.util.Deque;
15  import java.util.List;
16  import java.util.stream.Collectors;
17  
18  import static emissary.place.IServiceProviderPlace.SPROUT_KEY;
19  
20  public class TransformHistory implements Serializable {
21  
22      private static final long serialVersionUID = -7252497842562281631L;
23  
24      private final List<History> history;
25  
26      public TransformHistory() {
27          history = new ArrayList<>();
28      }
29  
30      public TransformHistory(TransformHistory history) {
31          this.history = new ArrayList<>(history.history);
32      }
33  
34      /**
35       * Replace history with the new history
36       *
37       * @param keys of new history strings to use
38       */
39      public void set(List<String> keys) {
40          clear();
41          addAll(keys.stream().map(History::new).collect(Collectors.toList()));
42      }
43  
44      /**
45       * Replace history with the new history
46       *
47       * @param history of new history strings to use
48       */
49      public void set(TransformHistory history) {
50          clear();
51          addAll(history.history);
52      }
53  
54      private void addAll(List<History> history) {
55          this.history.addAll(history);
56      }
57  
58      /**
59       * Appends the new key to the transform history. This is called by MobileAgent before moving to the new place. It
60       * usually adds the four-tuple of a place's key
61       *
62       * @see emissary.core.MobileAgent#agentControl
63       * @param key the new value to append
64       */
65      public void append(final String key) {
66          append(key, false);
67      }
68  
69      /**
70       * Appends the new key to the transform history. This is called by MobileAgent before moving to the new place. It
71       * usually adds the four-tuple of a place's key. Coordinated history keys are meant for informational purposes and have
72       * no bearing on the routing algorithm. It is important to list the places visited in coordination, but should not
73       * report as the last place visited.
74       *
75       * @see emissary.core.MobileAgent#agentControl
76       * @param key the new value to append
77       * @param coordinated true if history entry is for informational purposes only
78       */
79      public void append(String key, boolean coordinated) {
80          if (coordinated) {
81              History last = lastVisit();
82              if (last != null) {
83                  last.addCoordinated(key);
84              }
85          } else {
86              history.add(new History(key));
87          }
88  
89      }
90  
91      /**
92       * Clear the transformation history
93       */
94      public void clear() {
95          history.clear();
96      }
97  
98      /**
99       * For backwards compatibility, only return the history that does not contain the coordinated places
100      *
101      * @return List of places visited sans coordination places
102      */
103     public List<String> get() {
104         return get(false);
105     }
106 
107     /**
108      * Return the transform history, optionally including coordinated places
109      *
110      * @return List of places visited
111      */
112     public List<String> get(boolean includeCoordinated) {
113         if (includeCoordinated) {
114             List<String> keys = new ArrayList<>();
115             history.forEach(k -> {
116                 keys.add(k.getKey());
117                 keys.addAll(k.getCoordinated());
118             });
119             return keys;
120         } else {
121             return history.stream()
122                     .map(History::getKey)
123                     .collect(Collectors.toList());
124         }
125     }
126 
127     /**
128      * Return the full history object
129      *
130      * @return history object
131      */
132     public List<History> getHistory() {
133         return Collections.unmodifiableList(history);
134     }
135 
136     /**
137      * Get the last place visited (does not include places visited during coordination)
138      *
139      * @return last place visited
140      */
141     @Nullable
142     public History lastVisit() {
143         if (CollectionUtils.isEmpty(history)) {
144             return null;
145         }
146         return history.get(history.size() - 1);
147     }
148 
149     /**
150      * Get the second-to-last place visited (does not include places visited during coordination)
151      *
152      * @return second-to-last place visited
153      */
154     @Nullable
155     public History penultimateVisit() {
156         if (CollectionUtils.isEmpty(history) || history.size() < 2) {
157             return null;
158         }
159         return history.get(history.size() - 2);
160     }
161 
162     /**
163      * Test to see if a place has been visited (does not include places visited during coordination)
164      *
165      * @return true is place has been visited
166      */
167     public boolean hasVisited(final String pattern) {
168         for (final History hist : history) {
169             if (KeyManipulator.gmatch(hist.getKey(), pattern)) {
170                 return true;
171             }
172         }
173         return false;
174     }
175 
176     /**
177      * True if this payload hasn't had any processing yet. Does not count parent processing as being for this payload.
178      *
179      * @return true if not yet started
180      */
181     public boolean beforeStart() {
182         if (history.isEmpty()) {
183             return true;
184         }
185         final String s = history.get(history.size() - 1).getKey();
186         return s.contains(IServiceProviderPlace.SPROUT_KEY);
187     }
188 
189     public int size() {
190         return size(false);
191     }
192 
193     public int size(boolean includeCoordinated) {
194         int size = history.size();
195         if (includeCoordinated) {
196             size += (int) history.stream().mapToLong(h -> h.coordinated.size()).sum();
197         }
198         return size;
199     }
200 
201     public Deque<String> format() {
202         Deque<String> formattedHistory = new ArrayDeque<>();
203 
204         String prevDataAndServiceType = "";
205         for (final History h : this.history) {
206             String key = h.getKey();
207             String currentDataAndServiceType = "";
208             StringBuilder displayStrings = new StringBuilder();
209 
210             if (key.contains(SPROUT_KEY)) {
211                 displayStrings.append(StringUtils.substringBefore(formattedHistory.removeLast(), ".")).append(".")
212                         .append(KeyManipulator.getServiceType(key)).append(": ");
213                 while (!formattedHistory.isEmpty()) {
214                     String last = formattedHistory.removeLast();
215                     if (last.contains(SPROUT_KEY)) {
216                         formattedHistory.add(last);
217                         break;
218                     }
219                 }
220             } else {
221                 currentDataAndServiceType = KeyManipulator.getDataType(key) + "." + KeyManipulator.getServiceType(key);
222                 if (currentDataAndServiceType.equals(prevDataAndServiceType)) {
223                     displayStrings.append(formattedHistory.removeLast()).append(", ");
224                 } else {
225                     displayStrings.append(currentDataAndServiceType).append(": ");
226                 }
227             }
228 
229             displayStrings.append(KeyManipulator.getServiceClassname(key));
230             if (CollectionUtils.isNotEmpty(h.getCoordinated())) {
231                 displayStrings
232                         .append(h.getCoordinated().stream().map(KeyManipulator::getServiceClassname).collect(Collectors.joining(", ", "(", ")")));
233             }
234 
235             formattedHistory.add(displayStrings.toString());
236             prevDataAndServiceType = currentDataAndServiceType;
237         }
238         return formattedHistory;
239     }
240 
241     @Override
242     public String toString() {
243         final StringBuilder myOutput = new StringBuilder();
244         final String ls = System.lineSeparator();
245         myOutput.append("transform history (").append(size(true)).append(") :").append(ls);
246         history.forEach(x -> myOutput.append(x.toString()).append(ls));
247         return myOutput.toString();
248     }
249 
250     public static class History {
251         String key;
252         List<String> coordinated = new ArrayList<>();
253 
254         /**
255          * Needed to support Kryo deserialization
256          */
257         private History() {}
258 
259         public History(String key) {
260             this.key = key;
261         }
262 
263         public String getKey() {
264             return getKey(false);
265         }
266 
267         public String getKey(boolean stripUrl) {
268             return stripUrl ? stripUrl(key) : key;
269         }
270 
271         public List<String> getCoordinated() {
272             return getCoordinated(false);
273         }
274 
275         public List<String> getCoordinated(boolean stripUrl) {
276             return stripUrl ? coordinated.stream().map(History::stripUrl).collect(Collectors.toUnmodifiableList())
277                     : Collections.unmodifiableList(coordinated);
278         }
279 
280         public void addCoordinated(String key) {
281             coordinated.add(key);
282         }
283 
284         protected static String stripUrl(String key) {
285             return StringUtils.substringBefore(key, ".http");
286         }
287 
288         @Override
289         public String toString() {
290             StringBuilder hist = new StringBuilder("        -> " + getKey());
291             for (String coord : coordinated) {
292                 hist.append(System.lineSeparator()).append("           +--> ").append(coord);
293             }
294             return hist.toString();
295         }
296     }
297 }