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