d1264021b8f5ea3d4b282b9da71a4d241133bb5b
[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         String term = st.nextToken().trim();
239         if (!terms.contains(term))
240         {
241           terms.add(term);
242           count++;
243         }
244       }
245     }
246
247     /**
248      * Answers true if any of the patterns matches the value, else false
249      * 
250      * @param value
251      * @param filters
252      * @return
253      */
254     private boolean matches(String value, List<Pattern> filters)
255     {
256       for (Pattern p : filters)
257       {
258         if (p.matcher(value).matches())
259         {
260           return true;
261         }
262       }
263       return false;
264     }
265
266     /**
267      * Answers the description of the attribute, if recorded and unique, or null
268      * if either no, or more than description is recorded
269      * 
270      * @return
271      */
272     public String getDescription()
273     {
274       if (description != null && description.size() == 1)
275       {
276         return description.get(0);
277       }
278       return null;
279     }
280
281     public Datatype getType()
282     {
283       return type;
284     }
285
286     /**
287      * Adds the given description to the list of known descriptions (without
288      * duplication)
289      * 
290      * @param desc
291      */
292     public void addDescription(String desc)
293     {
294       if (desc != null)
295       {
296         if (description == null)
297         {
298           description = new ArrayList<>();
299         }
300         if (!description.contains(desc))
301         {
302           description.add(desc);
303         }
304       }
305     }
306
307     /**
308      * Answers the distinct terms recorded for the attribute, or an empty set if
309      * it is not configured to cache values
310      * 
311      * @return
312      */
313     public Set<String> getDistinctTerms()
314     {
315       return terms == null ? Collections.<String> emptySet() : terms;
316     }
317   }
318
319   /**
320    * Answers the singleton instance of this class
321    * 
322    * @return
323    */
324   public static FeatureAttributes getInstance()
325   {
326     return instance;
327   }
328
329   /**
330    * Private constructor to enforce singleton pattern
331    */
332   private FeatureAttributes()
333   {
334     attributes = new HashMap<>();
335     cachedAttributes = new HashSet<>();
336     uncachedAttributes = new HashSet<>();
337     cacheableNamePatterns = getFieldMatchers(CACHED_ATTS_KEY,
338             CACHED_ATTS_DEFAULT);
339   }
340
341   /**
342    * Reads the Preference value for the given key, with default specified if no
343    * preference set. The value is interpreted as a comma-separated list of
344    * regular expressions, and converted into a list of compiled patterns ready
345    * for matching. Patterns are set to non-case-sensitive matching.
346    * <p>
347    * This supports user-defined filters for attributes of interest to capture
348    * distinct values for as instance are added.
349    * 
350    * @param key
351    * @param def
352    * @return
353    */
354   public static List<Pattern> getFieldMatchers(String key, String def)
355   {
356     String pref = def;
357     try
358     {
359       // temporary for applet: handle class loading errors...
360       pref = Cache.getDefault(key, def);
361     } catch (Throwable t)
362     {
363     }
364     List<Pattern> patterns = new ArrayList<>();
365     String[] tokens = pref.split(",");
366     for (String token : tokens)
367     {
368       try
369       {
370         patterns.add(Pattern.compile(token, Pattern.CASE_INSENSITIVE));
371       } catch (PatternSyntaxException e)
372       {
373         System.err.println("Invalid pattern ignored: " + token);
374       }
375     }
376     return patterns;
377   }
378
379   /**
380    * Answers the attribute names known for the given feature type, in
381    * alphabetical order (not case sensitive), or an empty set if no attributes
382    * are known. An attribute name is typically 'simple' e.g. "AC", but may be
383    * 'compound' e.g. {"CSQ", "Allele"} where a feature has map-valued attributes
384    * 
385    * @param featureType
386    * @return
387    */
388   public List<String[]> getAttributes(String featureType)
389   {
390     if (!attributes.containsKey(featureType))
391     {
392       return Collections.<String[]> emptyList();
393     }
394
395     return new ArrayList<>(attributes.get(featureType).keySet());
396   }
397
398   /**
399    * Answers the set of distinct terms recorded for the given feature type and
400    * attribute. Answers an empty set if values are not cached for this
401    * attribute.
402    * 
403    * @param featureType
404    * @param attName
405    * @return
406    */
407   public Set<String> getDistinctTerms(String featureType, String... attName)
408   {
409     if (!attributes.containsKey(featureType)
410             || !attributes.get(featureType).containsKey(attName))
411     {
412       return Collections.<String> emptySet();
413     }
414
415     return attributes.get(featureType).get(attName).getDistinctTerms();
416   }
417
418   /**
419    * Answers true if at least one attribute is known for the given feature type,
420    * else false
421    * 
422    * @param featureType
423    * @return
424    */
425   public boolean hasAttributes(String featureType)
426   {
427     if (attributes.containsKey(featureType))
428     {
429       if (!attributes.get(featureType).isEmpty())
430       {
431         return true;
432       }
433     }
434     return false;
435   }
436
437   /**
438    * Records the given attribute name and description for the given feature
439    * type, and updates the min-max for any numeric value
440    * 
441    * @param featureType
442    * @param description
443    * @param value
444    * @param attName
445    */
446   public void addAttribute(String featureType, String description,
447           Object value, String... attName)
448   {
449     if (featureType == null || attName == null)
450     {
451       return;
452     }
453
454     /*
455      * if attribute value is a map, drill down one more level to
456      * record its sub-fields
457      */
458     if (value instanceof Map<?, ?>)
459     {
460       for (Entry<?, ?> entry : ((Map<?, ?>) value).entrySet())
461       {
462         String[] attNames = new String[attName.length + 1];
463         System.arraycopy(attName, 0, attNames, 0, attName.length);
464         attNames[attName.length] = entry.getKey().toString();
465         addAttribute(featureType, description, entry.getValue(), attNames);
466       }
467       return;
468     }
469
470     String valueAsString = value.toString();
471     Map<String[], AttributeData> atts = attributes.get(featureType);
472     if (atts == null)
473     {
474       atts = new TreeMap<>(comparator);
475       attributes.put(featureType, atts);
476     }
477     AttributeData attData = atts.get(attName);
478     if (attData == null)
479     {
480       attData = new AttributeData();
481       atts.put(attName, attData);
482     }
483     attData.addInstance(attName, description, valueAsString);
484   }
485
486   /**
487    * Answers the description of the given attribute for the given feature type,
488    * if known and unique, else null
489    * 
490    * @param featureType
491    * @param attName
492    * @return
493    */
494   public String getDescription(String featureType, String... attName)
495   {
496     String desc = null;
497     Map<String[], AttributeData> atts = attributes.get(featureType);
498     if (atts != null)
499     {
500       AttributeData attData = atts.get(attName);
501       if (attData != null)
502       {
503         desc = attData.getDescription();
504       }
505     }
506     return desc;
507   }
508
509   /**
510    * Answers the [min, max] value range of the given attribute for the given
511    * feature type, if known, else null. Attributes with a mixture of text and
512    * numeric values are considered text (do not return a min-max range).
513    * 
514    * @param featureType
515    * @param attName
516    * @return
517    */
518   public float[] getMinMax(String featureType, String... attName)
519   {
520     Map<String[], AttributeData> atts = attributes.get(featureType);
521     if (atts != null)
522     {
523       AttributeData attData = atts.get(attName);
524       if (attData != null && attData.hasValue)
525       {
526         return new float[] { attData.min, attData.max };
527       }
528     }
529     return null;
530   }
531
532   /**
533    * Records the given attribute description for the given feature type
534    * 
535    * @param featureType
536    * @param attName
537    * @param description
538    */
539   public void addDescription(String featureType, String description,
540           String... attName)
541   {
542     if (featureType == null || attName == null)
543     {
544       return;
545     }
546   
547     Map<String[], AttributeData> atts = attributes.get(featureType);
548     if (atts == null)
549     {
550       atts = new TreeMap<>(comparator);
551       attributes.put(featureType, atts);
552     }
553     AttributeData attData = atts.get(attName);
554     if (attData == null)
555     {
556       attData = new AttributeData();
557       atts.put(attName, attData);
558     }
559     attData.addDescription(description);
560   }
561
562   /**
563    * Answers the datatype of the feature, which is one of Character, Number or
564    * Mixed (or null if not known), as discovered from values recorded.
565    * 
566    * @param featureType
567    * @param attName
568    * @return
569    */
570   public Datatype getDatatype(String featureType, String... attName)
571   {
572     Map<String[], AttributeData> atts = attributes.get(featureType);
573     if (atts != null)
574     {
575       AttributeData attData = atts.get(attName);
576       if (attData != null)
577       {
578         return attData.getType();
579       }
580     }
581     return null;
582   }
583 }