View Javadoc
1   package emissary.directory;
2   
3   import jakarta.annotation.Nullable;
4   import org.slf4j.Logger;
5   import org.slf4j.LoggerFactory;
6   
7   import java.util.ArrayList;
8   import java.util.List;
9   import java.util.Map;
10  import java.util.concurrent.ConcurrentHashMap;
11  
12  /**
13   * Keep a map of DataID to DirectoryEntryList for the Directory Extensible to use other things for the key if desired,
14   * just override the methods that figure out the key automatically from the DirectoryEntry or DirectoryEntryList.
15   */
16  public class DirectoryEntryMap extends ConcurrentHashMap<String, DirectoryEntryList> {
17  
18      // Serializable
19      static final long serialVersionUID = 9097156614421373808L;
20  
21      private static final Logger logger = LoggerFactory.getLogger(DirectoryEntryMap.class);
22  
23      /** Value of DEEP_COPY flag */
24      public static final boolean DEEP_COPY = true;
25  
26      /** Value of SHALLOW_COPY flag */
27      public static final boolean SHALLOW_COPY = false;
28  
29      /**
30       * No arg ctor supplies our tuned defaults to the super ctor
31       */
32      public DirectoryEntryMap() {
33          super(1024, 0.8f, 3);
34      }
35  
36      /**
37       * Capacity ctor
38       * 
39       * @param initialCapacity initial capacity for map
40       */
41      public DirectoryEntryMap(final int initialCapacity) {
42          super(initialCapacity, 0.8f, 3);
43      }
44  
45      /**
46       * Full ctor
47       * 
48       * @param initialCapacity initial capacity for map
49       * @param loadFactor how loaded before rehash required
50       * @param concurrencyLevel how many threads can update at once
51       */
52      public DirectoryEntryMap(final int initialCapacity, final float loadFactor, final int concurrencyLevel) {
53          super(initialCapacity, loadFactor, concurrencyLevel);
54      }
55  
56      /**
57       * Create a new map from an old one. Makes a shallow copy
58       * 
59       * @param map the map to copy
60       */
61      public DirectoryEntryMap(final DirectoryEntryMap map) {
62          this(map, SHALLOW_COPY);
63      }
64  
65      /**
66       * Create a new map from an old one.
67       * 
68       * @param map the map to copy
69       * @param deepCopy true if should be a deep copy
70       */
71      public DirectoryEntryMap(@Nullable final DirectoryEntryMap map, final boolean deepCopy) {
72          this();
73          if (map != null) {
74              for (final Map.Entry<String, DirectoryEntryList> entry : map.entrySet()) {
75                  if (deepCopy) {
76                      final DirectoryEntryList dl = new DirectoryEntryList(entry.getValue(), DirectoryEntryList.DEEP_COPY);
77                      this.put(entry.getKey(), dl);
78                  } else {
79                      this.put(entry.getKey(), entry.getValue());
80                  }
81              }
82          }
83      }
84  
85      /**
86       * Add a directory entry to the appropriate DirectoryEntryList If it is a duplicate entry in all parts except cost, only
87       * the lowest cost entry is kept. Either this entry or the one already in the list will be discarded.
88       *
89       * @param d the entry to add
90       */
91      public void addEntry(final DirectoryEntry d) {
92          final String dataId = KeyManipulator.getDataId(d.getKey());
93          addEntry(dataId, d);
94      }
95  
96      /**
97       * Add the directory entry to the specified list
98       * 
99       * @param key the key to this map
100      * @param d the entry to add
101      */
102     protected void addEntry(final String key, final DirectoryEntry d) {
103         DirectoryEntryList list = get(key);
104 
105         if (list == null) {
106             list = new DirectoryEntryList();
107             put(key, list);
108         }
109         final int beforeSize = list.size();
110         list.add(d);
111         final int afterSize = list.size();
112 
113         if (logger.isDebugEnabled()) {
114             // This check could be wrong since nothing is synchronized.
115             // Its understood and ok since it's just debug stuff
116             if (afterSize > beforeSize) {
117                 logger.debug("Appears to have added entry {} list is now size {}", d.getKey(), afterSize);
118             } else {
119                 logger.debug("Appear to have dropped or replaced entry {} list is still size {}", d.getKey(), afterSize);
120             }
121         }
122     }
123 
124     /**
125      * Builds a super list of all the entries currently in all buckets of the map
126      * 
127      * @return list of all directory entries
128      */
129     public List<DirectoryEntry> allEntries() {
130         final List<DirectoryEntry> list = new ArrayList<>();
131 
132         for (final DirectoryEntryList d : values()) {
133             list.addAll(d);
134         }
135         return list;
136     }
137 
138     /**
139      * Builds a super list of all the entry keys currently in all buckets of the map
140      * 
141      * @return list of all directory entry keys
142      */
143     public List<String> allEntryKeys() {
144         final List<String> list = new ArrayList<>();
145 
146         for (final DirectoryEntryList d : values()) {
147             for (final DirectoryEntry e : d) {
148                 list.add(e.getFullKey());
149             }
150         }
151         return list;
152     }
153 
154     /**
155      * Perform a super count of all the DirectoryEntry objects on all of the lists
156      * 
157      * @return count of all DirectoryEntry contained in map
158      */
159     public int entryCount() {
160         int count = 0;
161         for (final DirectoryEntryList d : values()) {
162             count += d.size();
163         }
164         return count;
165     }
166 
167     /**
168      * Remove a directory entry from the map
169      * 
170      * @param entryKey for the entry to remove, not wildcarded
171      * @return the removed entry or null if not found
172      */
173     public DirectoryEntry removeEntry(final String entryKey) {
174         final String dataId = KeyManipulator.getDataId(entryKey);
175         return removeEntry(dataId, entryKey);
176     }
177 
178     /**
179      * Remove the directory entry specified by entry key from the list specified by key
180      * 
181      * @param key the key to this map
182      * @param entryKey the key of the DirectoryEntry to remove
183      * @return the removed object
184      */
185     protected DirectoryEntry removeEntry(final String key, final String entryKey) {
186         DirectoryEntry removed = null;
187         final DirectoryEntryList list = get(key);
188         if (list != null) {
189             // NB: cannot remove from DirectoryEntryList through iterator
190             for (int i = 0; i < list.size(); i++) {
191                 final DirectoryEntry entry = list.get(i);
192                 if (entry.getKey().equals(entryKey)) {
193                     removed = entry;
194                     list.remove(i);
195                     break;
196                 }
197             }
198 
199             // Remove the mapping if it is empty
200             if (list.isEmpty()) {
201                 this.remove(key);
202             }
203         }
204 
205         return removed;
206     }
207 
208     /**
209      * Remove all entries that match the key returning them as a list
210      * 
211      * @param key the key that must be matched (can be wildcarded)
212      * @return list of entries that were removed from the map
213      */
214     public List<DirectoryEntry> removeAllMatching(final String key) {
215         return removeAllMatching(key, Long.MAX_VALUE);
216     }
217 
218     /**
219      * Remove all entries that match the key returning them as a list
220      * 
221      * @param key the key that must be matched (can be wildcarded)
222      * @param checkpoint only remove entries older than this value, use Long.MAX_VALUE to have all matching entries removed
223      *        without regard to their age
224      * @return list of entries that were removed from the map
225      */
226     public List<DirectoryEntry> removeAllMatching(final String key, final long checkpoint) {
227         final List<DirectoryEntry> removed = new ArrayList<>();
228 
229         for (final DirectoryEntryList list : values()) {
230             // NB: cannot remove from DirectoryEntryList through iterator
231             // Need to mark and sweep
232             for (int i = 0; i < list.size(); i++) {
233                 final DirectoryEntry entry = list.get(i);
234                 if (KeyManipulator.gmatch(entry.getKey(), key) && entry.getAge() < checkpoint) {
235                     removed.add(entry);
236                 }
237             }
238         }
239 
240         for (final DirectoryEntry d : removed) {
241             removeEntry(d.getKey());
242         }
243 
244         return removed;
245     }
246 
247     /**
248      * Find and remove all entries on a directory
249      * 
250      * @param key the key of the directory
251      * @return list of all entries removed
252      */
253     public List<DirectoryEntry> removeAllOnDirectory(final String key) {
254         // Wildcard the key so we can just use gmatch
255         final String wckey = KeyManipulator.getHostMatchKey(key);
256         return removeAllMatching(wckey);
257     }
258 
259     /**
260      * Find and remove all entries on a directory
261      * 
262      * @param key the key of the directory
263      * @param checkpoint only remove entries older than this
264      * @return list of all entries removed
265      */
266     public List<DirectoryEntry> removeAllOnDirectory(final String key, final long checkpoint) {
267         // Wildcard the key so we can just use gmatch
268         final String wckey = KeyManipulator.getHostMatchKey(key);
269         return removeAllMatching(wckey, checkpoint);
270     }
271 
272     /**
273      * Collect all entries that match the key returning them as a list
274      * 
275      * @param key the key that must be matched (can be wildcarded)
276      * @return list of entries that were matches, still live in the directory map
277      */
278     public List<DirectoryEntry> collectAllMatching(final String key) {
279         final List<DirectoryEntry> match = new ArrayList<>();
280 
281         final String dataId = KeyManipulator.getDataId(key);
282         if (dataId.contains("*") || dataId.contains("?")) {
283             for (final DirectoryEntryList list : values()) {
284                 for (final DirectoryEntry entry : list) {
285                     if (KeyManipulator.gmatch(entry.getKey(), key)) {
286                         match.add(entry);
287                     }
288                 }
289             }
290         } else {
291             final DirectoryEntryList list = this.get(dataId);
292             if (list != null) {
293                 for (final DirectoryEntry entry : list) {
294                     if (KeyManipulator.gmatch(entry.getKey(), key)) {
295                         match.add(entry);
296                     }
297                 }
298             }
299         }
300         return match;
301     }
302 
303     /**
304      * Find all entries on a directory
305      * 
306      * @param key the key of the directory
307      * @return list of all entries matched, still live in directory map
308      */
309     public List<DirectoryEntry> collectAllOnDirectory(final String key) {
310         // Wildcard the key so we can just use gmatch
311         final String wckey = KeyManipulator.getHostMatchKey(key);
312         return collectAllMatching(wckey);
313     }
314 
315     /**
316      * Count all entries that match the key
317      * 
318      * @param key the key that must be matched (can be wildcarded)
319      * @return count of entries that were matches
320      */
321     public int countAllMatching(final String key) {
322         int count = 0;
323 
324         for (final DirectoryEntryList list : values()) {
325             for (DirectoryEntry entry : list) {
326                 if (KeyManipulator.gmatch(entry.getKey(), key)) {
327                     count++;
328                 }
329             }
330         }
331         return count;
332     }
333 
334     /**
335      * Count all entries on a directory
336      * 
337      * @param key the key of the directory
338      * @return count of all entries matched
339      */
340     public int countAllOnDirectory(final String key) {
341         // Wildcard the key so we can just use gmatch
342         final String wckey = KeyManipulator.getHostMatchKey(key);
343         return countAllMatching(wckey);
344     }
345 
346     /**
347      * Merge new entries in our local entry map
348      *
349      * @param entryList the list of entries to merge
350      */
351     public void addEntries(@Nullable final List<DirectoryEntry> entryList) {
352         if (entryList != null) {
353             for (final DirectoryEntry d : entryList) {
354                 addEntry(d);
355             }
356         }
357     }
358 
359     /**
360      * Merge new non-local peer entries into our local entry map.
361      * 
362      * @param that the new entries
363      */
364     public void addEntries(@Nullable final DirectoryEntryMap that) {
365         if (that != null) {
366             for (final Map.Entry<String, DirectoryEntryList> entry : that.entrySet()) {
367                 // Optimized add since already grouped by same key
368                 DirectoryEntryList list = this.get(entry.getKey());
369                 if (list == null) {
370                     list = new DirectoryEntryList();
371                     put(entry.getKey(), list);
372                 }
373                 list.addAll(entry.getValue());
374             }
375         }
376     }
377 
378     /**
379      * Change cost on matching entries
380      * 
381      * @param key the key to match
382      * @param increment cost increment
383      * @return List of string entry keys changed
384      */
385     public List<String> addCostToMatching(final String key, final int increment) {
386         final List<DirectoryEntry> list = collectAllMatching(key);
387         final List<String> ret = new ArrayList<>();
388         for (final DirectoryEntry e : list) {
389             e.addCost(increment);
390             ret.add(e.getFullKey());
391         }
392 
393         // Put them back in order if something changed
394         if (!ret.isEmpty()) {
395             sort();
396         }
397 
398         return ret;
399     }
400 
401     /**
402      * Force a sort on all the directory entry lists due to some external factors
403      */
404     public void sort() {
405         for (final DirectoryEntryList list : values()) {
406             list.sort();
407         }
408     }
409 }