001/*
002 *                    BioJava development code
003 *
004 * This code may be freely distributed and modified under the
005 * terms of the GNU Lesser General Public Licence.  This should
006 * be distributed with the code.  If you do not have a copy,
007 * see:
008 *
009 *      http://www.gnu.org/copyleft/lesser.html
010 *
011 * Copyright for this code is held jointly by the individual
012 * authors.  These should be listed in @author doc comments.
013 *
014 * For more information on the BioJava project and its aims,
015 * or to join the biojava-l mailing list, visit the home page
016 * at:
017 *
018 *      http://www.biojava.org/
019 *
020 * Created on 01-21-2010
021 */
022package org.biojava.nbio.core.sequence.location;
023
024import org.biojava.nbio.core.exceptions.ParserException;
025import org.biojava.nbio.core.sequence.AccessionID;
026import org.biojava.nbio.core.sequence.DataSource;
027import org.biojava.nbio.core.sequence.Strand;
028import org.biojava.nbio.core.sequence.location.template.AbstractLocation;
029import org.biojava.nbio.core.sequence.location.template.Location;
030import org.biojava.nbio.core.sequence.location.template.Point;
031
032import java.util.ArrayList;
033import java.util.List;
034import java.util.regex.Matcher;
035import java.util.regex.Pattern;
036
037/**
038 * Parser for working with INSDC style locations. This class supports the
039 * full range of location types generated by Genbank, INSDC and ENA.
040 *
041 * @author ayates
042 * @author jgrzebyta
043 * @author Paolo Pavan
044 */
045public class InsdcParser {
046
047        private boolean isSequenceCircular;
048        private long sequenceLength;
049
050        private final DataSource dataSource;
051
052                /**
053         * parse a location. if group(1) is null than the feature is on the positive
054         * strand, group(2) start position, group(3) end position.
055         */
056        protected static final Pattern singleLocationPattern = Pattern.compile("(?:([A-Za-z\\.0-9_]*?):)?(<?)(\\d+)(\\.{2}|\\^)?(>?)(\\d+)?(>?)?");
057        /**
058         * Decodes a split pattern. Split patterns are a composition of multiple
059         * locationsString qualified by actions: join(location,location, ...
060         * location): The indicated elements should be joined (placed end-to-end) to
061         * form one contiguous sequence. order(location,location, ... location): The
062         * elements can be found in the specified order (5' to 3' direction),
063         * nothing is implied about their reasonableness
064         * bond(location,location...location): Found in protein files. These
065         * generally are used to describe disulfide bonds.
066         * complement(location,location...location): consider locations in their
067         * complement versus
068         *
069         * takes in input a comma split location string. The split must be done
070         * for outer level commas group(1) is the qualifier group(2) is the location
071         * string to getFeatures. In case of complex splits it will contain the
072         * nested expression
073         *
074         * Not really sure that they are not declared obsolete but they are still in
075         * several files.
076         */
077        protected static final Pattern genbankSplitPattern = Pattern.compile("^\\s?(join|order|bond|complement|)\\(?([\\s\\S]+)\\)?");
078        /**
079         * designed to recursively split a location string in tokens. Valid tokens
080         * are those divided by coma that are not inside a bracket. I. e. split on
081         * the comma only if that comma has zero, or an even number of quotes ahead
082         * of it.
083         */
084        protected static final String locationSplitPattern = ",(?=([^\\(|\\)]+\\([^\\(|\\)]+\\))[^\\(|\\)]+)";
085        /**
086         * these variables are used to compute the global start and end of complex
087         * features
088         */
089        protected Integer featureGlobalStart, featureGlobalEnd;
090
091        enum complexFeaturesAppendEnum {
092
093                FLATTEN, HIERARCHICAL;
094        }
095        /**
096         * define the mode in which complex features should be appended in FLATTEN
097         * mode their will be appended to a single feature in HIERARCHICAL mode, the
098         * single mother feature will have a tree of features that will reflect the
099         * construction in genbank file
100         */
101        private complexFeaturesAppendEnum complexFeaturesAppendMode = complexFeaturesAppendEnum.HIERARCHICAL;
102
103        public void setComplexFeaturesAppendMode(complexFeaturesAppendEnum complexFeaturesAppendMode) {
104                this.complexFeaturesAppendMode = complexFeaturesAppendMode;
105        }
106
107        public InsdcParser() {
108                this(DataSource.ENA);
109        }
110
111        public InsdcParser(DataSource dataSource) {
112                this.dataSource = dataSource;
113        }
114
115        public DataSource getDataSource() {
116                return dataSource;
117        }
118
119        public void setSequenceCircular(boolean sequenceCircular) {
120                isSequenceCircular = sequenceCircular;
121        }
122
123        public void setSequenceLength(long sequenceLength) {
124                this.sequenceLength = sequenceLength;
125        }
126
127        /**
128         * Main method for parsing a location from a String instance
129         *
130         * @param locationString Represents a logical location
131         * @return The parsed location
132         * @throws ParserException thrown in the event of any error during parsing
133         */
134        public Location parse(String locationString) throws ParserException {
135                featureGlobalStart = Integer.MAX_VALUE;
136                featureGlobalEnd = 1;
137
138                Location l;
139                List<Location> ll = parseLocationString(locationString, 1);
140
141                if (ll.size() == 1) {
142                        l = ll.get(0);
143                } else {
144                        l = new SimpleLocation(
145                                        new SimplePoint(featureGlobalStart),
146                                        new SimplePoint(featureGlobalEnd),
147                                        Strand.UNDEFINED,
148                                        isSequenceCircular,
149                                        ll);
150                }
151                return l;
152        }
153
154        private List<Location> parseLocationString(String string, int versus) throws ParserException {
155                Matcher m;
156                List<Location> boundedLocationsCollection = new ArrayList<Location>();
157
158                List<String> tokens = splitString(string);
159                for (String t : tokens) {
160                        m = genbankSplitPattern.matcher(t);
161                        if (!m.find()) {
162                                throw new ParserException("Cannot interpret split pattern " + t
163                                                + "\nin location string:" + string);
164                        }
165                        String splitQualifier = m.group(1);
166                        String splitString = m.group(2);
167
168                        if (!splitQualifier.isEmpty()) {
169                                //recursive case
170                                int localVersus = splitQualifier.equalsIgnoreCase("complement") ? -1 : 1;
171                                List<Location> subLocations = parseLocationString(
172                                                splitString, versus * localVersus);
173
174                                switch (complexFeaturesAppendMode) {
175                                        case FLATTEN:
176                                                boundedLocationsCollection.addAll(subLocations);
177                                                break;
178                                        case HIERARCHICAL:
179                                                if (subLocations.size() == 1) {
180                                                        boundedLocationsCollection.addAll(subLocations);
181                                                } else {
182                                                        Point min = Location.Tools.getMin(subLocations).getStart();
183                                                        Point max = Location.Tools.getMax(subLocations).getEnd();
184                                                        AbstractLocation motherLocation
185                                                                        = new SimpleLocation(
186                                                                                        min,
187                                                                                        max
188                                                                        );
189
190                                                        if (splitQualifier.equalsIgnoreCase("join")) {
191                                                                motherLocation = new InsdcLocations.GroupLocation(subLocations);
192                                                        }
193                                                        if (splitQualifier.equalsIgnoreCase("order")) {
194                                                                motherLocation = new InsdcLocations.OrderLocation(subLocations);
195                                                        }
196                                                        if (splitQualifier.equalsIgnoreCase("bond")) {
197                                                                motherLocation = new InsdcLocations.BondLocation(subLocations);
198                                                        }
199                                                        motherLocation.setStrand(getGroupLocationStrand(subLocations));
200                                                        boundedLocationsCollection.add(motherLocation);
201                                                }
202                                        break;
203                                }
204                        } else {
205                                //base case
206                                m = singleLocationPattern.matcher(splitString);
207                                if (!m.find()) {
208                                        throw new ParserException("Cannot interpret location pattern " + splitString
209                                                        + "\nin location string:" + string);
210                                }
211
212                                String accession = m.group(1);
213                                Strand s = versus == 1 ? Strand.POSITIVE : Strand.NEGATIVE;
214                                int start = Integer.valueOf(m.group(3));
215                                int end = m.group(6) == null ? start : Integer.valueOf(m.group(6));
216
217                                if (featureGlobalStart > start) {
218                                        featureGlobalStart = start;
219                                }
220                                if (featureGlobalEnd < end) {
221                                        featureGlobalEnd = end;
222                                }
223
224                                AbstractLocation l;
225                                if (start <= end) {
226                                        l = new SimpleLocation(
227                                                        start,
228                                                        end,
229                                                        s
230                                        );
231                                } else {
232                                        // in case of location spanning the end point, Location contract wants sublocations
233                                        AbstractLocation l5prime = new SimpleLocation(
234                                                        1,
235                                                        end,
236                                                        Strand.UNDEFINED
237                                                        );
238                                        AbstractLocation l3prime = new SimpleLocation(
239                                                        start,
240                                                        (int) sequenceLength,
241                                                        Strand.UNDEFINED
242                                                        );
243
244                                        l = new InsdcLocations.GroupLocation(
245                                                        new SimplePoint(start),
246                                                        new SimplePoint(end),
247                                                        s,
248                                                        isSequenceCircular,
249                                                        l5prime, l3prime
250                                        );
251
252                                }
253
254                                if(m.group(4) != null && m.group(4).equals("^")) l.setBetweenCompounds(true);
255
256                                if (m.group(2).equals("<")) {
257                                        l.setPartialOn5prime(true);
258                                }
259                                if (m.group(5) != null && (m.group(5).equals(">") || m.group(7).equals(">"))) {
260                                        l.setPartialOn3prime(true);
261                                }
262
263                                if (!(accession == null || "".equals(accession))) l.setAccession(new AccessionID(accession));
264
265                                boundedLocationsCollection.add(l);
266
267                        }
268                }
269
270                return boundedLocationsCollection;
271        }
272
273
274        private List<String> splitString(String input) {
275                List<String> result = new ArrayList<String>();
276                int start = 0;
277                int openedParenthesis = 0;
278                for (int current = 0; current < input.length(); current++) {
279                        if (input.charAt(current) == '(') {
280                                openedParenthesis++;
281                        }
282                        if (input.charAt(current) == ')') {
283                                openedParenthesis--;
284                        }
285                        boolean atLastChar = (current == input.length() - 1);
286                        if (atLastChar) {
287                                result.add(input.substring(start));
288                        } else if (input.charAt(current) == ',' && openedParenthesis == 0) {
289                                result.add(input.substring(start, current));
290                                start = current + 1;
291                        }
292                }
293                return result;
294        }
295
296        private Strand getGroupLocationStrand(List<Location> ll){
297                Strand returnStrand = null;
298
299                for (Location l: ll) {
300                        if (returnStrand == null) returnStrand = l.getStrand();
301                        if (returnStrand != l.getStrand()) return Strand.UNDEFINED;
302                }
303                return returnStrand;
304        }
305
306}