JAL-3020 limit cache size; handle applet exception
[jalview.git] / src / jalview / datamodel / features / FeatureAttributes.java
1 package jalview.datamodel.features;
2
3 import jalview.bin.Cache;
4
5 import java.util.ArrayList;
6 import java.util.Collections;
7 import java.util.Comparator;
8 import java.util.HashMap;
9 import java.util.HashSet;
10 import java.util.List;
11 import java.util.Map;
12 import java.util.Map.Entry;
13 import java.util.Set;
14 import java.util.StringTokenizer;
15 import java.util.TreeMap;
16 import java.util.regex.Pattern;
17 import java.util.regex.PatternSyntaxException;
18
19 /**
20  * A singleton class to hold the set of attributes known for each feature type
21  */
22 public class FeatureAttributes
23 {
24   public enum Datatype
25   {
26     Character, Number, Mixed
27   }
28
29   /*
30    * property key for lookup of a comma-separated list of regex patterns
31    * to match those attribute names for which distinct values should be cached
32    */
33   private static final String CACHED_ATTS_KEY = "CACHED_ATTRIBUTES";
34
35   /*
36    * default value if property is not specified
37    * (selected VCF/VEP terms which have 'categorical' value ranges)
38    */
39   private static final String CACHED_ATTS_DEFAULT = "AS_FilterStatus,clinical_significance,consequence_type,"
40           + "CSQ:Consequence,CSQ:CLIN_SIG,CSQ:DOMAIN,CSQ:IMPACT";
41
42   /*
43    * delimiters of terms in attribute values
44    */
45   private static final String TERM_DELIMITERS = ",&";
46
47   /*
48    * defensive limit to number of attribute values cached per attribute
49    */
50   private static final int MAX_ATT_VALS = 30;
51
52   private static FeatureAttributes instance = new FeatureAttributes();
53
54   /*
55    * map, by feature type, of a map, by attribute name, of
56    * attribute description and min-max range (if known)
57    */
58   private Map<String, Map<String[], AttributeData>> attributes;
59
60   /*
61    * attribute names that have been seen and 
62    * match the condition for caching distinct values
63    */
64   private Set<String> cachedAttributes;
65
66   /*
67    * attribute names that have been seen and do not 
68    * match the condition for caching distinct values
69    */
70   private Set<String> uncachedAttributes;
71
72   private List<Pattern> cacheableNamePatterns;
73
74   /*
75    * a case-insensitive comparator so that attributes are ordered e.g.
76    * AC
77    * af
78    * CSQ:AFR_MAF
79    * CSQ:Allele
80    */
81   private Comparator<String[]> comparator = new Comparator<String[]>()
82   {
83     @Override
84     public int compare(String[] o1, String[] o2)
85     {
86       int i = 0;
87       while (i < o1.length || i < o2.length)
88       {
89         if (o2.length <= i)
90         {
91           return o1.length <= i ? 0 : 1;
92         }
93         if (o1.length <= i)
94         {
95           return -1;
96         }
97         int comp = String.CASE_INSENSITIVE_ORDER.compare(o1[i], o2[i]);
98         if (comp != 0)
99         {
100           return comp;
101         }
102         i++;
103       }
104       return 0; // same length and all matched
105     }
106   };
107
108   private class AttributeData
109   {
110     /*
111      * description(s) for this attribute, if known
112      * (different feature source might have differing descriptions)
113      */
114     List<String> description;
115
116     /*
117      * minimum value (if only numeric values recorded)
118      */
119     float min = 0f;
120
121     /*
122      * maximum value (if only numeric values recorded)
123      */
124     float max = 0f;
125
126     /*
127      * flag is set true if only numeric values are detected for this attribute
128      */
129     boolean hasValue = false;
130
131     Datatype type;
132
133     /*
134      * (for selected attributes), a list of distinct terms found in values
135      */
136     Set<String> terms;
137
138     /**
139      * Note one instance of this attribute, recording unique, non-null
140      * descriptions, and the min/max of any numerical values.
141      * <p>
142      * Distinct value terms may also be recorded, if the feature type is one for
143      * which this is configured
144      * 
145      * @param attName
146      * @param desc
147      * @param value
148      */
149     void addInstance(String[] attName, String desc, String value)
150     {
151       addDescription(desc);
152
153       if (value != null)
154       {
155         value = value.trim();
156
157         String name = FeatureMatcher.toAttributeDisplayName(attName);
158         recordValue(name, value);
159
160         /*
161          * Parse numeric value unless we have previously
162          * seen text data for this attribute type
163          */
164         if (type == null || type == Datatype.Number)
165         {
166           try
167           {
168             float f = Float.valueOf(value);
169             min = hasValue ? Float.min(min, f) : f;
170             max = hasValue ? Float.max(max, f) : f;
171             hasValue = true;
172             type = (type == null || type == Datatype.Number)
173                     ? Datatype.Number
174                     : Datatype.Mixed;
175           } catch (NumberFormatException e)
176           {
177             /*
178              * non-numeric data: treat attribute as Character (or Mixed)
179              */
180             type = (type == null || type == Datatype.Character)
181                     ? Datatype.Character
182                     : Datatype.Mixed;
183             min = 0f;
184             max = 0f;
185             hasValue = false;
186           }
187         }
188       }
189     }
190
191     /**
192      * If attribute name is configured to cache distinct values, then parse out
193      * and store these
194      * 
195      * @param attName
196      * @param value
197      */
198     private void recordValue(String attName, String value)
199     {
200       /*
201        * quit if we've seen this attribute name before,
202        * and determined we are not caching its values
203        */
204       if (uncachedAttributes.contains(attName))
205       {
206         return;
207       }
208
209       /*
210        * if first time seen, check attribute name filters to
211        * see if we want to cache its value
212        */
213       if (!cachedAttributes.contains(attName))
214       {
215         if (!matches(attName, cacheableNamePatterns))
216         {
217           uncachedAttributes.add(attName);
218           return;
219         }
220         else
221         {
222           cachedAttributes.add(attName);
223         }
224       }
225
226       /*
227        * we want to cache distinct terms for this attribute;
228        * parse them out using comma or & delimiters
229        */
230       if (terms == null)
231       {
232         terms = new HashSet<>();
233       }
234       int count = terms.size();
235       StringTokenizer st = new StringTokenizer(value, TERM_DELIMITERS);
236       while (st.hasMoreTokens() && count < MAX_ATT_VALS)
237       {
238         terms.add(st.nextToken().trim());
239         count++;
240       }
241     }
242
243     /**
244      * Answers true if any of the patterns matches the value, else false
245      * 
246      * @param value
247      * @param filters
248      * @return
249      */
250     private boolean matches(String value, List<Pattern> filters)
251     {
252       for (Pattern p : filters)
253       {
254         if (p.matcher(value).matches())
255         {
256           return true;
257         }
258       }
259       return false;
260     }
261
262     /**
263      * Answers the description of the attribute, if recorded and unique, or null
264      * if either no, or more than description is recorded
265      * 
266      * @return
267      */
268     public String getDescription()
269     {
270       if (description != null && description.size() == 1)
271       {
272         return description.get(0);
273       }
274       return null;
275     }
276
277     public Datatype getType()
278     {
279       return type;
280     }
281
282     /**
283      * Adds the given description to the list of known descriptions (without
284      * duplication)
285      * 
286      * @param desc
287      */
288     public void addDescription(String desc)
289     {
290       if (desc != null)
291       {
292         if (description == null)
293         {
294           description = new ArrayList<>();
295         }
296         if (!description.contains(desc))
297         {
298           description.add(desc);
299         }
300       }
301     }
302
303     /**
304      * Answers the distinct terms recorded for the attribute, or an empty set if
305      * it is not configured to cache values
306      * 
307      * @return
308      */
309     public Set<String> getDistinctTerms()
310     {
311       return terms == null ? Collections.<String> emptySet() : terms;
312     }
313   }
314
315   /**
316    * Answers the singleton instance of this class
317    * 
318    * @return
319    */
320   public static FeatureAttributes getInstance()
321   {
322     return instance;
323   }
324
325   /**
326    * Private constructor to enforce singleton pattern
327    */
328   private FeatureAttributes()
329   {
330     attributes = new HashMap<>();
331     cachedAttributes = new HashSet<>();
332     uncachedAttributes = new HashSet<>();
333     cacheableNamePatterns = getFieldMatchers(CACHED_ATTS_KEY,
334             CACHED_ATTS_DEFAULT);
335   }
336
337   /**
338    * Reads the Preference value for the given key, with default specified if no
339    * preference set. The value is interpreted as a comma-separated list of
340    * regular expressions, and converted into a list of compiled patterns ready
341    * for matching. Patterns are set to non-case-sensitive matching.
342    * <p>
343    * This supports user-defined filters for attributes of interest to capture
344    * distinct values for as instance are added.
345    * 
346    * @param key
347    * @param def
348    * @return
349    */
350   public static List<Pattern> getFieldMatchers(String key, String def)
351   {
352     String pref = def;
353     try
354     {
355       // temporary for applet: handle class loading errors...
356       pref = Cache.getDefault(key, def);
357     } catch (Throwable t)
358     {
359     }
360     List<Pattern> patterns = new ArrayList<>();
361     String[] tokens = pref.split(",");
362     for (String token : tokens)
363     {
364       try
365       {
366         patterns.add(Pattern.compile(token, Pattern.CASE_INSENSITIVE));
367       } catch (PatternSyntaxException e)
368       {
369         System.err.println("Invalid pattern ignored: " + token);
370       }
371     }
372     return patterns;
373   }
374
375   /**
376    * Answers the attribute names known for the given feature type, in
377    * alphabetical order (not case sensitive), or an empty set if no attributes
378    * are known. An attribute name is typically 'simple' e.g. "AC", but may be
379    * 'compound' e.g. {"CSQ", "Allele"} where a feature has map-valued attributes
380    * 
381    * @param featureType
382    * @return
383    */
384   public List<String[]> getAttributes(String featureType)
385   {
386     if (!attributes.containsKey(featureType))
387     {
388       return Collections.<String[]> emptyList();
389     }
390
391     return new ArrayList<>(attributes.get(featureType).keySet());
392   }
393
394   /**
395    * Answers the set of distinct terms recorded for the given feature type and
396    * attribute. Answers an empty set if values are not cached for this
397    * attribute.
398    * 
399    * @param featureType
400    * @param attName
401    * @return
402    */
403   public Set<String> getDistinctTerms(String featureType, String... attName)
404   {
405     if (!attributes.containsKey(featureType)
406             || !attributes.get(featureType).containsKey(attName))
407     {
408       return Collections.<String> emptySet();
409     }
410
411     return attributes.get(featureType).get(attName).getDistinctTerms();
412   }
413
414   /**
415    * Answers true if at least one attribute is known for the given feature type,
416    * else false
417    * 
418    * @param featureType
419    * @return
420    */
421   public boolean hasAttributes(String featureType)
422   {
423     if (attributes.containsKey(featureType))
424     {
425       if (!attributes.get(featureType).isEmpty())
426       {
427         return true;
428       }
429     }
430     return false;
431   }
432
433   /**
434    * Records the given attribute name and description for the given feature
435    * type, and updates the min-max for any numeric value
436    * 
437    * @param featureType
438    * @param description
439    * @param value
440    * @param attName
441    */
442   public void addAttribute(String featureType, String description,
443           Object value, String... attName)
444   {
445     if (featureType == null || attName == null)
446     {
447       return;
448     }
449
450     /*
451      * if attribute value is a map, drill down one more level to
452      * record its sub-fields
453      */
454     if (value instanceof Map<?, ?>)
455     {
456       for (Entry<?, ?> entry : ((Map<?, ?>) value).entrySet())
457       {
458         String[] attNames = new String[attName.length + 1];
459         System.arraycopy(attName, 0, attNames, 0, attName.length);
460         attNames[attName.length] = entry.getKey().toString();
461         addAttribute(featureType, description, entry.getValue(), attNames);
462       }
463       return;
464     }
465
466     String valueAsString = value.toString();
467     Map<String[], AttributeData> atts = attributes.get(featureType);
468     if (atts == null)
469     {
470       atts = new TreeMap<>(comparator);
471       attributes.put(featureType, atts);
472     }
473     AttributeData attData = atts.get(attName);
474     if (attData == null)
475     {
476       attData = new AttributeData();
477       atts.put(attName, attData);
478     }
479     attData.addInstance(attName, description, valueAsString);
480   }
481
482   /**
483    * Answers the description of the given attribute for the given feature type,
484    * if known and unique, else null
485    * 
486    * @param featureType
487    * @param attName
488    * @return
489    */
490   public String getDescription(String featureType, String... attName)
491   {
492     String desc = null;
493     Map<String[], AttributeData> atts = attributes.get(featureType);
494     if (atts != null)
495     {
496       AttributeData attData = atts.get(attName);
497       if (attData != null)
498       {
499         desc = attData.getDescription();
500       }
501     }
502     return desc;
503   }
504
505   /**
506    * Answers the [min, max] value range of the given attribute for the given
507    * feature type, if known, else null. Attributes with a mixture of text and
508    * numeric values are considered text (do not return a min-max range).
509    * 
510    * @param featureType
511    * @param attName
512    * @return
513    */
514   public float[] getMinMax(String featureType, String... attName)
515   {
516     Map<String[], AttributeData> atts = attributes.get(featureType);
517     if (atts != null)
518     {
519       AttributeData attData = atts.get(attName);
520       if (attData != null && attData.hasValue)
521       {
522         return new float[] { attData.min, attData.max };
523       }
524     }
525     return null;
526   }
527
528   /**
529    * Records the given attribute description for the given feature type
530    * 
531    * @param featureType
532    * @param attName
533    * @param description
534    */
535   public void addDescription(String featureType, String description,
536           String... attName)
537   {
538     if (featureType == null || attName == null)
539     {
540       return;
541     }
542   
543     Map<String[], AttributeData> atts = attributes.get(featureType);
544     if (atts == null)
545     {
546       atts = new TreeMap<>(comparator);
547       attributes.put(featureType, atts);
548     }
549     AttributeData attData = atts.get(attName);
550     if (attData == null)
551     {
552       attData = new AttributeData();
553       atts.put(attName, attData);
554     }
555     attData.addDescription(description);
556   }
557
558   /**
559    * Answers the datatype of the feature, which is one of Character, Number or
560    * Mixed (or null if not known), as discovered from values recorded.
561    * 
562    * @param featureType
563    * @param attName
564    * @return
565    */
566   public Datatype getDatatype(String featureType, String... attName)
567   {
568     Map<String[], AttributeData> atts = attributes.get(featureType);
569     if (atts != null)
570     {
571       AttributeData attData = atts.get(attName);
572       if (attData != null)
573       {
574         return attData.getType();
575       }
576     }
577     return null;
578   }
579 }