X-Git-Url: http://source.jalview.org/gitweb/?a=blobdiff_plain;ds=sidebyside;f=src%2Fjalview%2Fio%2FFeaturesFile.java;h=821384a600dc2176a332ef00e5bbd18452fc5d27;hb=refs%2Fheads%2FJAL-3253_applet_omnibus-BH;hp=a3cab6b8151715e5422494479ba2a6751bfe8306;hpb=7e2aa9039133d9e17aef05507fc652542d64a838;p=jalview.git diff --git a/src/jalview/io/FeaturesFile.java b/src/jalview/io/FeaturesFile.java index a3cab6b..821384a 100755 --- a/src/jalview/io/FeaturesFile.java +++ b/src/jalview/io/FeaturesFile.java @@ -24,14 +24,18 @@ import jalview.analysis.AlignmentUtils; import jalview.analysis.SequenceIdMatcher; import jalview.api.AlignViewportI; import jalview.api.FeatureColourI; +import jalview.api.FeatureRenderer; import jalview.api.FeaturesSourceI; import jalview.datamodel.AlignedCodonFrame; import jalview.datamodel.Alignment; import jalview.datamodel.AlignmentI; +import jalview.datamodel.MappedFeatures; import jalview.datamodel.SequenceDummy; import jalview.datamodel.SequenceFeature; import jalview.datamodel.SequenceI; -import jalview.io.gff.GffHelperBase; +import jalview.datamodel.features.FeatureMatcherSet; +import jalview.datamodel.features.FeatureMatcherSetI; +import jalview.gui.Desktop; import jalview.io.gff.GffHelperFactory; import jalview.io.gff.GffHelperI; import jalview.schemes.FeatureColour; @@ -45,11 +49,12 @@ import java.io.IOException; import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; -import java.util.Comparator; import java.util.HashMap; +import java.util.LinkedHashMap; import java.util.List; import java.util.Map; import java.util.Map.Entry; +import java.util.TreeMap; /** * Parses and writes features files, which may be in Jalview, GFF2 or GFF3 @@ -69,27 +74,24 @@ import java.util.Map.Entry; */ public class FeaturesFile extends AlignFile implements FeaturesSourceI { + private static final String EQUALS = "="; + + private static final String TAB_REGEX = "\\t"; + + private static final String STARTGROUP = "STARTGROUP"; + + private static final String ENDGROUP = "ENDGROUP"; + + private static final String STARTFILTERS = "STARTFILTERS"; + + private static final String ENDFILTERS = "ENDFILTERS"; + private static final String ID_NOT_SPECIFIED = "ID_NOT_SPECIFIED"; private static final String NOTE = "Note"; - protected static final String TAB = "\t"; - protected static final String GFF_VERSION = "##gff-version"; - private static final Comparator SORT_NULL_LAST = new Comparator() - { - @Override - public int compare(String o1, String o2) - { - if (o1 == null) - { - return o2 == null ? 0 : 1; - } - return (o2 == null ? -1 : o1.compareTo(o2)); - } - }; - private AlignmentI lastmatchedAl = null; private SequenceIdMatcher matcher = null; @@ -108,14 +110,14 @@ public class FeaturesFile extends AlignFile implements FeaturesSourceI /** * Constructor which does not parse the file immediately * - * @param inFile + * @param file File or String filename * @param paste * @throws IOException */ - public FeaturesFile(String inFile, DataSourceType paste) + public FeaturesFile(Object file, DataSourceType paste) throws IOException { - super(false, inFile, paste); + super(false, file, paste); } /** @@ -131,15 +133,14 @@ public class FeaturesFile extends AlignFile implements FeaturesSourceI * Constructor that optionally parses the file immediately * * @param parseImmediately - * @param inFile + * @param file * @param type * @throws IOException */ - public FeaturesFile(boolean parseImmediately, String inFile, - DataSourceType type) - throws IOException + public FeaturesFile(boolean parseImmediately, Object file, + DataSourceType type) throws IOException { - super(parseImmediately, inFile, type); + super(parseImmediately, file, type); } /** @@ -184,7 +185,7 @@ public class FeaturesFile extends AlignFile implements FeaturesSourceI * @param align * - alignment/dataset containing sequences that are to be annotated * @param colours - * - hashtable to store feature colour definitions + * - map to store feature colour definitions * @param removeHTML * - process html strings into plain text * @param relaxedIdmatching @@ -195,11 +196,34 @@ public class FeaturesFile extends AlignFile implements FeaturesSourceI Map colours, boolean removeHTML, boolean relaxedIdmatching) { - Map gffProps = new HashMap(); + return parse(align, colours, null, removeHTML, relaxedIdmatching); + } + + /** + * Parse GFF or Jalview format sequence features file + * + * @param align + * - alignment/dataset containing sequences that are to be annotated + * @param colours + * - map to store feature colour definitions + * @param filters + * - map to store feature filter definitions + * @param removeHTML + * - process html strings into plain text + * @param relaxedIdmatching + * - when true, ID matches to compound sequence IDs are allowed + * @return true if features were added + */ + public boolean parse(AlignmentI align, + Map colours, + Map filters, boolean removeHTML, + boolean relaxedIdmatching) + { + Map gffProps = new HashMap<>(); /* * keep track of any sequences we try to create from the data */ - List newseqs = new ArrayList(); + List newseqs = new ArrayList<>(); String line = null; try @@ -219,7 +243,7 @@ public class FeaturesFile extends AlignFile implements FeaturesSourceI continue; } - gffColumns = line.split("\\t"); // tab as regex + gffColumns = line.split(TAB_REGEX); if (gffColumns.length == 1) { if (line.trim().equalsIgnoreCase("GFF")) @@ -233,18 +257,23 @@ public class FeaturesFile extends AlignFile implements FeaturesSourceI } } - if (gffColumns.length > 1 && gffColumns.length < 4) + if (gffColumns.length > 0 && gffColumns.length < 4) { /* * if 2 or 3 tokens, we anticipate either 'startgroup', 'endgroup' or * a feature type colour specification */ String ft = gffColumns[0]; - if (ft.equalsIgnoreCase("startgroup")) + if (ft.equalsIgnoreCase(STARTFILTERS)) + { + parseFilters(filters); + continue; + } + if (ft.equalsIgnoreCase(STARTGROUP)) { featureGroup = gffColumns[1]; } - else if (ft.equalsIgnoreCase("endgroup")) + else if (ft.equalsIgnoreCase(ENDGROUP)) { // We should check whether this is the current group, // but at present there's no way of showing more than 1 group @@ -305,6 +334,43 @@ public class FeaturesFile extends AlignFile implements FeaturesSourceI } /** + * Reads input lines from STARTFILTERS to ENDFILTERS and adds a feature type + * filter to the map for each line parsed. After exit from this method, + * nextLine() should return the line after ENDFILTERS (or we are already at + * end of file if ENDFILTERS was missing). + * + * @param filters + * @throws IOException + */ + protected void parseFilters(Map filters) + throws IOException + { + String line; + while ((line = nextLine()) != null) + { + if (line.toUpperCase().startsWith(ENDFILTERS)) + { + return; + } + String[] tokens = line.split(TAB_REGEX); + if (tokens.length != 2) + { + System.err.println(String.format("Invalid token count %d for %d", + tokens.length, line)); + } + else + { + String featureType = tokens[0]; + FeatureMatcherSetI fm = FeatureMatcherSet.fromString(tokens[1]); + if (fm != null && filters != null) + { + filters.put(featureType, fm); + } + } + } + } + + /** * Try to parse a Jalview format feature specification and add it as a * sequence feature to any matching sequences in the alignment. Returns true * if successful (a feature was added), or false if not. @@ -319,7 +385,8 @@ public class FeaturesFile extends AlignFile implements FeaturesSourceI */ protected boolean parseJalviewFeature(String line, String[] gffColumns, AlignmentI alignment, Map featureColours, - boolean removeHTML, boolean relaxedIdMatching, String featureGroup) + boolean removeHTML, boolean relaxedIdMatching, + String featureGroup) { /* * tokens: description seqid seqIndex start end type [score] @@ -373,20 +440,23 @@ public class FeaturesFile extends AlignFile implements FeaturesSourceI Color colour = ColorUtils.createColourFromName(ft); featureColours.put(ft, new FeatureColour(colour)); } - SequenceFeature sf = new SequenceFeature(ft, desc, "", startPos, - endPos, featureGroup); + SequenceFeature sf = null; if (gffColumns.length > 6) { float score = Float.NaN; try { - score = new Float(gffColumns[6]).floatValue(); - // update colourgradient bounds if allowed to + score = Float.valueOf(gffColumns[6]).floatValue(); } catch (NumberFormatException ex) { - // leave as NaN + sf = new SequenceFeature(ft, desc, startPos, endPos, featureGroup); } - sf.setScore(score); + sf = new SequenceFeature(ft, desc, startPos, endPos, score, + featureGroup); + } + else + { + sf = new SequenceFeature(ft, desc, startPos, endPos, featureGroup); } parseDescriptionHTML(sf, removeHTML); @@ -498,39 +568,48 @@ public class FeaturesFile extends AlignFile implements FeaturesSourceI } /** - * Returns contents of a Jalview format features file, for visible features, - * as filtered by type and group. Features with a null group are displayed if - * their feature type is visible. Non-positional features may optionally be - * included (with no check on type or group). + * Returns contents of a Jalview format features file, for visible features, as + * filtered by type and group. Features with a null group are displayed if their + * feature type is visible. Non-positional features may optionally be included + * (with no check on type or group). * * @param sequences - * source of features - * @param visible - * map of colour for each visible feature type - * @param visibleFeatureGroups + * @param fr * @param includeNonPositional - * if true, include non-positional features (regardless of group or - * type) + * if true, include non-positional features + * (regardless of group or type) + * @param includeComplement + * if true, include visible complementary + * (CDS/protein) positional features, with + * locations converted to local sequence + * coordinates * @return */ public String printJalviewFormat(SequenceI[] sequences, - Map visible, - List visibleFeatureGroups, boolean includeNonPositional) + FeatureRenderer fr, boolean includeNonPositional, + boolean includeComplement) { - if (!includeNonPositional && (visible == null || visible.isEmpty())) - { - // no point continuing. - return "No Features Visible"; - } + Map visibleColours = fr + .getDisplayedFeatureCols(); + Map featureFilters = fr.getFeatureFilters(); + + // BH check this is out? +// if (!includeNonPositional +// && (visibleColours == null || visibleColours.isEmpty())) +// { +// // no point continuing. +// return "No Features Visible"; +// } /* * write out feature colours (if we know them) */ // TODO: decide if feature links should also be written here ? StringBuilder out = new StringBuilder(256); - if (visible != null) + if (visibleColours != null) { - for (Entry featureColour : visible.entrySet()) + for (Entry featureColour : visibleColours + .entrySet()) { FeatureColourI colour = featureColour.getValue(); out.append(colour.toJalviewFormat(featureColour.getKey())).append( @@ -538,90 +617,311 @@ public class FeaturesFile extends AlignFile implements FeaturesSourceI } } - String[] types = visible == null ? new String[0] : visible.keySet() - .toArray(new String[visible.keySet().size()]); + String[] types = visibleColours == null ? new String[0] + : visibleColours.keySet() + .toArray(new String[visibleColours.keySet().size()]); /* - * sort groups alphabetically, and ensure that features with a - * null or empty group are output after those in named groups + * feature filters if any */ - List sortedGroups = new ArrayList(visibleFeatureGroups); - sortedGroups.remove(null); - sortedGroups.remove(""); - Collections.sort(sortedGroups); - sortedGroups.add(null); - sortedGroups.add(""); + outputFeatureFilters(out, visibleColours, featureFilters); + + /* + * output features within groups + */ + int count = outputFeaturesByGroup(out, fr, types, sequences, + includeNonPositional); + + if (includeComplement) + { + count += outputComplementFeatures(out, fr, sequences); + } - boolean foundSome = false; + return count > 0 ? out.toString() : "No Features Visible"; + } + + /** + * Outputs any visible complementary (CDS/peptide) positional features as + * Jalview format, within feature group. The coordinates of the linked features + * are converted to the corresponding positions of the local sequences. + * + * @param out + * @param fr + * @param sequences + * @return + */ + private int outputComplementFeatures(StringBuilder out, + FeatureRenderer fr, SequenceI[] sequences) + { + AlignViewportI comp = fr.getViewport().getCodingComplement(); + FeatureRenderer fr2 = Desktop.getAlignFrameFor(comp) + .getFeatureRenderer(); /* - * first output any non-positional features + * bin features by feature group and sequence */ - if (includeNonPositional) + Map>> map = new TreeMap<>( + String.CASE_INSENSITIVE_ORDER); + int count = 0; + + for (SequenceI seq : sequences) { - for (int i = 0; i < sequences.length; i++) + /* + * find complementary features + */ + List complementary = findComplementaryFeatures(seq, + fr2); + String seqName = seq.getName(); + + for (SequenceFeature sf : complementary) { - String sequenceName = sequences[i].getName(); - for (SequenceFeature feature : sequences[i].getFeatures() - .getNonPositionalFeatures()) + String group = sf.getFeatureGroup(); + if (!map.containsKey(group)) + { + map.put(group, new LinkedHashMap<>()); // preserves sequence order + } + Map> groupFeatures = map.get(group); + if (!groupFeatures.containsKey(seqName)) { - foundSome = true; - out.append(formatJalviewFeature(sequenceName, feature)); + groupFeatures.put(seqName, new ArrayList<>()); } + List foundFeatures = groupFeatures.get(seqName); + foundFeatures.add(sf); + count++; } } - for (String group : sortedGroups) + /* + * output features by group + */ + for (Entry>> groupFeatures : map.entrySet()) { - boolean isNamedGroup = (group != null && !"".equals(group)); - if (isNamedGroup) + out.append(newline); + String group = groupFeatures.getKey(); + if (!"".equals(group)) { - out.append(newline); - out.append("STARTGROUP").append(TAB); - out.append(group); - out.append(newline); + out.append(STARTGROUP).append(TAB).append(group).append(newline); + } + Map> seqFeaturesMap = groupFeatures + .getValue(); + for (Entry> seqFeatures : seqFeaturesMap + .entrySet()) + { + String sequenceName = seqFeatures.getKey(); + for (SequenceFeature sf : seqFeatures.getValue()) + { + formatJalviewFeature(out, sequenceName, sf); + } + } + if (!"".equals(group)) + { + out.append(ENDGROUP).append(TAB).append(group).append(newline); + } + } + + return count; + } + + /** + * Answers a list of mapped features visible in the (CDS/protein) complement, + * with feature positions translated to local sequence coordinates + * + * @param seq + * @param fr2 + * @return + */ + protected List findComplementaryFeatures(SequenceI seq, + FeatureRenderer fr2) + { + /* + * avoid duplication of features (e.g. peptide feature + * at all 3 mapped codon positions) + */ + List found = new ArrayList<>(); + List complementary = new ArrayList<>(); + + for (int pos = seq.getStart(); pos <= seq.getEnd(); pos++) + { + MappedFeatures mf = fr2.findComplementFeaturesAtResidue(seq, pos); + + if (mf != null) + { + MapList mapping = mf.mapping.getMap(); + for (SequenceFeature sf : mf.features) + { + /* + * make a virtual feature with local coordinates + */ + if (!found.contains(sf)) + { + String group = sf.getFeatureGroup(); + if (group == null) + { + group = ""; + } + found.add(sf); + int begin = sf.getBegin(); + int end = sf.getEnd(); + int[] range = mf.mapping.getTo() == seq.getDatasetSequence() + ? mapping.locateInTo(begin, end) + : mapping.locateInFrom(begin, end); + SequenceFeature sf2 = new SequenceFeature(sf, range[0], + range[1], group, sf.getScore()); + complementary.add(sf2); + } + } } + } + + return complementary; + } + + /** + * Outputs any feature filters defined for visible feature types, sandwiched by + * STARTFILTERS and ENDFILTERS lines + * + * @param out + * @param visible + * @param featureFilters + */ + void outputFeatureFilters(StringBuilder out, + Map visible, + Map featureFilters) + { + if (visible == null || featureFilters == null + || featureFilters.isEmpty()) + { + return; + } + + boolean first = true; + for (String featureType : visible.keySet()) + { + FeatureMatcherSetI filter = featureFilters.get(featureType); + if (filter != null) + { + if (first) + { + first = false; + out.append(newline).append(STARTFILTERS).append(newline); + } + out.append(featureType).append(TAB).append(filter.toStableString()) + .append(newline); + } + } + if (!first) + { + out.append(ENDFILTERS).append(newline); + } + + } + + /** + * Appends output of visible sequence features within feature groups to the + * output buffer. Groups other than the null or empty group are sandwiched by + * STARTGROUP and ENDGROUP lines. Answers the number of features written. + * + * @param out + * @param fr + * @param featureTypes + * @param sequences + * @param includeNonPositional + * @return + */ + private int outputFeaturesByGroup(StringBuilder out, + FeatureRenderer fr, String[] featureTypes, + SequenceI[] sequences, boolean includeNonPositional) + { + List featureGroups = fr.getFeatureGroups(); + + /* + * sort groups alphabetically, and ensure that features with a + * null or empty group are output after those in named groups + */ + List sortedGroups = new ArrayList<>(featureGroups); + sortedGroups.remove(null); + sortedGroups.remove(""); + Collections.sort(sortedGroups); + sortedGroups.add(null); + sortedGroups.add(""); + + int count = 0; + List visibleGroups = fr.getDisplayedFeatureGroups(); + + /* + * loop over all groups (may be visible or not); + * non-positional features are output even if group is not visible + */ + for (String group : sortedGroups) + { + boolean firstInGroup = true; + boolean isNullGroup = group == null || "".equals(group); - /* - * output positional features within groups - */ for (int i = 0; i < sequences.length; i++) { String sequenceName = sequences[i].getName(); - List features = new ArrayList(); - if (types.length > 0) + List features = new ArrayList<>(); + + /* + * get any non-positional features in this group, if wanted + * (for any feature type, whether visible or not) + */ + if (includeNonPositional) + { + features.addAll(sequences[i].getFeatures() + .getFeaturesForGroup(false, group)); + } + + /* + * add positional features for visible feature types, but + * (for named groups) only if feature group is visible + */ + if (featureTypes.length > 0 + && (isNullGroup || visibleGroups.contains(group))) { features.addAll(sequences[i].getFeatures().getFeaturesForGroup( - true, group, types)); + true, group, featureTypes)); } - for (SequenceFeature sequenceFeature : features) + for (SequenceFeature sf : features) { - foundSome = true; - out.append(formatJalviewFeature(sequenceName, sequenceFeature)); + if (sf.isNonPositional() || fr.isVisible(sf)) + { + count++; + if (firstInGroup) + { + out.append(newline); + if (!isNullGroup) + { + out.append(STARTGROUP).append(TAB).append(group) + .append(newline); + } + } + firstInGroup = false; + formatJalviewFeature(out, sequenceName, sf); + } } } - if (isNamedGroup) + if (!isNullGroup && !firstInGroup) { - out.append("ENDGROUP").append(TAB); - out.append(group); - out.append(newline); + out.append(ENDGROUP).append(TAB).append(group).append(newline); } } - - return foundSome ? out.toString() : "No Features Visible"; + return count; } /** + * Formats one feature in Jalview format and appends to the string buffer + * * @param out * @param sequenceName * @param sequenceFeature */ protected String formatJalviewFeature( - String sequenceName, SequenceFeature sequenceFeature) + StringBuilder out, String sequenceName, + SequenceFeature sequenceFeature) { - StringBuilder out = new StringBuilder(64); if (sequenceFeature.description == null || sequenceFeature.description.equals("")) { @@ -646,7 +946,8 @@ public class FeaturesFile extends AlignFile implements FeaturesSourceI if (sequenceFeature.description.indexOf(href) == -1) { - out.append(" " + label + ""); + out.append(" ") + .append(label).append(""); } } @@ -684,14 +985,15 @@ public class FeaturesFile extends AlignFile implements FeaturesSourceI AlignViewportI av = getViewport(); if (av != null) { - if (av.getAlignment() != null) + AlignmentI a = av.getAlignment(); + if (a != null) { - dataset = av.getAlignment().getDataset(); + dataset = a.getDataset(); } if (dataset == null) { // working in the applet context ? - dataset = av.getAlignment(); + dataset = a; } } else @@ -699,7 +1001,7 @@ public class FeaturesFile extends AlignFile implements FeaturesSourceI dataset = new Alignment(new SequenceI[] {}); } - Map featureColours = new HashMap(); + Map featureColours = new HashMap<>(); boolean parseResult = parse(dataset, featureColours, false, true); if (!parseResult) { @@ -736,49 +1038,80 @@ public class FeaturesFile extends AlignFile implements FeaturesSourceI * a map whose keys are the type names of visible features * @param visibleFeatureGroups * @param includeNonPositionalFeatures + * @param includeComplement * @return */ public String printGffFormat(SequenceI[] sequences, - Map visible, - List visibleFeatureGroups, - boolean includeNonPositionalFeatures) + FeatureRenderer fr, boolean includeNonPositionalFeatures, + boolean includeComplement) { + FeatureRenderer fr2 = null; + if (includeComplement) + { + AlignViewportI comp = fr.getViewport().getCodingComplement(); + fr2 = Desktop.getAlignFrameFor(comp).getFeatureRenderer(); + } + + Map visibleColours = fr.getDisplayedFeatureCols(); + StringBuilder out = new StringBuilder(256); out.append(String.format("%s %d\n", GFF_VERSION, gffVersion == 0 ? 2 : gffVersion)); - if (!includeNonPositionalFeatures - && (visible == null || visible.isEmpty())) - { - return out.toString(); - } - - String[] types = visible == null ? new String[0] : visible.keySet() - .toArray( - new String[visible.keySet().size()]); + String[] types = visibleColours == null ? new String[0] + : visibleColours.keySet() + .toArray(new String[visibleColours.keySet().size()]); for (SequenceI seq : sequences) { - List features = new ArrayList(); + List seqFeatures = new ArrayList<>(); + List features = new ArrayList<>(); if (includeNonPositionalFeatures) { features.addAll(seq.getFeatures().getNonPositionalFeatures()); } - if (visible != null && !visible.isEmpty()) + if (visibleColours != null && !visibleColours.isEmpty()) { features.addAll(seq.getFeatures().getPositionalFeatures(types)); } for (SequenceFeature sf : features) { - String source = sf.featureGroup; - if (!sf.isNonPositional() && source != null - && !visibleFeatureGroups.contains(source)) + if (sf.isNonPositional() || fr.isVisible(sf)) { - // group is not visible - continue; + /* + * drop features hidden by group visibility, colour threshold, + * or feature filter condition + */ + seqFeatures.add(sf); } + } + if (includeComplement) + { + seqFeatures.addAll(findComplementaryFeatures(seq, fr2)); + } + + /* + * sort features here if wanted + */ + for (SequenceFeature sf : seqFeatures) + { + formatGffFeature(out, seq, sf); + out.append(newline); + } + } + + return out.toString(); + } + + /** + * Formats one feature as GFF and appends to the string buffer + */ + private void formatGffFeature(StringBuilder out, SequenceI seq, + SequenceFeature sf) + { + String source = sf.featureGroup; if (source == null) { source = sf.getDescription(); @@ -804,18 +1137,111 @@ public class FeaturesFile extends AlignFile implements FeaturesSourceI String phase = sf.getPhase(); out.append(phase == null ? "." : phase); - // miscellaneous key-values (GFF column 9) - String attributes = sf.getAttributes(); - if (attributes != null) + if (sf.otherDetails != null && !sf.otherDetails.isEmpty()) { - out.append(TAB).append(attributes); - } + Map map = sf.otherDetails; + formatAttributes(out, map); + } + } - out.append(newline); + /** + * A helper method that outputs attributes stored in the map as + * semicolon-delimited values e.g. + * + *
+   * AC_Male=0;AF_NFE=0.00000e 00;Hom_FIN=0;GQ_MEDIAN=9
+   * 
+ * + * A map-valued attribute is formatted as a comma-delimited list within braces, + * for example + * + *
+   * jvmap_CSQ={ALLELE_NUM=1,UNIPARC=UPI0002841053,Feature=ENST00000585561}
+   * 
+ * + * The {@code jvmap_} prefix designates a values map and is removed if the value + * is parsed when read in. (The GFF3 specification allows 'semi-structured data' + * to be represented provided the attribute name begins with a lower case + * letter.) + * + * @param sb + * @param map + * @see http://gmod.org/wiki/GFF3#GFF3_Format + */ + void formatAttributes(StringBuilder sb, Map map) + { + sb.append(TAB); + boolean first = true; + for (String key : map.keySet()) + { + if (SequenceFeature.STRAND.equals(key) + || SequenceFeature.PHASE.equals(key)) + { + /* + * values stashed in map but output to their own columns + */ + continue; + } + { + if (!first) + { + sb.append(";"); + } + } + first = false; + Object value = map.get(key); + if (value instanceof Map) + { + formatMapAttribute(sb, key, (Map) value); + } + else + { + String formatted = StringUtils.urlEncode(value.toString(), + GffHelperI.GFF_ENCODABLE); + sb.append(key).append(EQUALS).append(formatted); } } + } - return out.toString(); + /** + * Formats the map entries as + * + *
+   * key=key1=value1,key2=value2,...
+   * 
+ * + * and appends this to the string buffer + * + * @param sb + * @param key + * @param map + */ + private void formatMapAttribute(StringBuilder sb, String key, + Map map) + { + if (map == null || map.isEmpty()) + { + return; + } + + /* + * AbstractMap.toString would be a shortcut here, but more reliable + * to code the required format in case toString changes in future + */ + sb.append(key).append(EQUALS); + boolean first = true; + for (Entry entry : map.entrySet()) + { + if (!first) + { + sb.append(","); + } + first = false; + sb.append(entry.getKey().toString()).append(EQUALS); + String formatted = StringUtils.urlEncode(entry.getValue().toString(), + GffHelperI.GFF_ENCODABLE); + sb.append(formatted); + } } /** @@ -873,8 +1299,8 @@ public class FeaturesFile extends AlignFile implements FeaturesSourceI fromCount = Integer.parseInt(tokens[2]); } catch (NumberFormatException nfe) { - throw new IOException("Invalid number in Align field: " - + nfe.getMessage()); + throw new IOException( + "Invalid number in Align field: " + nfe.getMessage()); } /* @@ -962,37 +1388,38 @@ public class FeaturesFile extends AlignFile implements FeaturesSourceI return seq; } - /** - * Process the 'column 9' data of the GFF file. This is less formally defined, - * and its interpretation will vary depending on the tool that has generated - * it. - * - * @param attributes - * @param sf - */ - protected void processGffColumnNine(String attributes, SequenceFeature sf) - { - sf.setAttributes(attributes); - - /* - * Parse attributes in column 9 and add them to the sequence feature's - * 'otherData' table; use Note as a best proxy for description - */ - char nameValueSeparator = gffVersion == 3 ? '=' : ' '; - // TODO check we don't break GFF2 values which include commas here - Map> nameValues = GffHelperBase - .parseNameValuePairs(attributes, ";", nameValueSeparator, ","); - for (Entry> attr : nameValues.entrySet()) - { - String values = StringUtils.listToDelimitedString(attr.getValue(), - "; "); - sf.setValue(attr.getKey(), values); - if (NOTE.equals(attr.getKey())) - { - sf.setDescription(values); - } - } - } + // BH! check that we did not lose something here. +// /** +// * Process the 'column 9' data of the GFF file. This is less formally defined, +// * and its interpretation will vary depending on the tool that has generated +// * it. +// * +// * @param attributes +// * @param sf +// */ +// protected void processGffColumnNine(String attributes, SequenceFeature sf) +// { +// sf.setAttributes(attributes); +// +// /* +// * Parse attributes in column 9 and add them to the sequence feature's +// * 'otherData' table; use Note as a best proxy for description +// */ +// char nameValueSeparator = gffVersion == 3 ? '=' : ' '; +// // TODO check we don't break GFF2 values which include commas here +// Map> nameValues = GffHelperBase +// .parseNameValuePairs(attributes, ";", nameValueSeparator, ","); +// for (Entry> attr : nameValues.entrySet()) +// { +// String values = StringUtils.listToDelimitedString(attr.getValue(), +// "; "); +// sf.setValue(attr.getKey(), values); +// if (NOTE.equals(attr.getKey())) +// { +// sf.setDescription(values); +// } +// } +// } /** * After encountering ##fasta in a GFF3 file, process the remainder of the @@ -1095,9 +1522,8 @@ public class FeaturesFile extends AlignFile implements FeaturesSourceI * @param newseqs * @throws IOException */ - protected void processGffPragma(String line, - Map gffProps, AlignmentI align, - List newseqs) throws IOException + protected void processGffPragma(String line, Map gffProps, + AlignmentI align, List newseqs) throws IOException { line = line.trim(); if ("###".equals(line))