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 */
021package org.biojava.nbio.structure;
022
023import org.biojava.nbio.structure.align.util.AtomCache;
024import org.biojava.nbio.structure.io.PDBFileReader;
025import org.biojava.nbio.structure.io.cif.CifStructureConverter;
026import org.biojava.nbio.structure.io.StructureFiletype;
027import org.biojava.nbio.structure.io.mmtf.MmtfActions;
028import org.slf4j.Logger;
029import org.slf4j.LoggerFactory;
030
031import java.io.IOException;
032import java.io.UnsupportedEncodingException;
033import java.net.MalformedURLException;
034import java.net.URL;
035import java.net.URLDecoder;
036import java.util.Collections;
037import java.util.LinkedHashMap;
038import java.util.List;
039import java.util.Map;
040import java.util.regex.Matcher;
041import java.util.regex.Pattern;
042
043/**
044 * Represents a structure loaded from a URL (including a file URL)
045 *
046 * A few custom query parameters are supported:
047 *
048 * <ul>
049 * <li><tt>format=[pdb|cif]</tt> Specify the file format (will otherwise be
050 *     guessed from the extension)
051 * <li><tt>pdbId=[String]</tt> Specify the PDB ID (also guessed from the filename)
052 * <li><tt>chainID=[String]</tt> A single chain from the structure
053 * <li><tt>residues=[String]</tt> Residue ranges, in a form understood by
054 *     {@link SubstructureIdentifier}
055 * </ul>
056 * @author Spencer Bliven
057 *
058 */
059public class URLIdentifier implements StructureIdentifier {
060        private static final long serialVersionUID = -5161230822868926035L;
061        private static final Logger logger = LoggerFactory.getLogger(URLIdentifier.class);
062
063        // Used for guessing the PDB ID from the filename
064        //UPDATE: It seems that this RegEx rarely succeeded , because the file
065        //name is most of the time in the format pdbxxxx.EXT not xxxx.EXT.
066        private static final Pattern PDBID_REGEX = Pattern.compile("^(?:pdb)?([0-9][a-z0-9]{3})([._-]|\\s).*", Pattern.CASE_INSENSITIVE);
067//      private static final Pattern PDBID_REGEX = Pattern.compile("^(?:pdb)?((PDB_[0-9]{4})?[0-9][a-z0-9]{3})([._-]|\\s).*", Pattern.CASE_INSENSITIVE);
068        
069        /** URL parameter specifying the file format (PDB or CIF) */
070        public static final String FORMAT_PARAM = "format";
071        /** URL parameter specifying the PDB ID */
072        public static final String PDBID_PARAM = "pdbid";
073        /** URL parameter specifying a single chain to include; overridden by residues */
074
075        //TODO: should this get renamed to chainname or asymid?
076        public static final String CHAINID_PARAM = "chainid";
077        /**
078         * URL parameter specifying residue ranges to include, e.g. <tt>residues=A:1-70</tt>
079         * @see SubstructureIdentifier
080         */
081        public static final String RESIDUES_PARAM = "residues";
082
083        final private URL url;
084        public URLIdentifier(URL url) {
085                this.url = url;
086        }
087
088        public URLIdentifier(String url) throws MalformedURLException {
089                this(new URL(url));
090        }
091
092        public URL getURL() {
093                return url;
094        }
095
096        @Override
097        public String getIdentifier() {
098                return url.toString();
099        }
100
101        /**
102         * @return A SubstructureIdentifier without ranges (e.g. including all residues)
103         */
104        @Override
105        public SubstructureIdentifier toCanonical() throws StructureException{
106                String pdbId = null;
107                List<ResidueRange> ranges = Collections.emptyList();
108                try {
109                        Map<String, String> params = parseQuery(url);
110                        if (params.containsKey(PDBID_PARAM)) {
111                                pdbId = params.get(PDBID_PARAM);
112                        }
113                        if (params.containsKey(RESIDUES_PARAM)) {
114                                ranges = ResidueRange.parseMultiple(params.get(RESIDUES_PARAM));
115                        } else if (params.containsKey(CHAINID_PARAM)) {
116                                ranges = Collections.singletonList(new ResidueRange(params.get(CHAINID_PARAM), (ResidueNumber) null, (ResidueNumber) null));
117                        }
118                } catch (UnsupportedEncodingException e) {
119                        logger.error("Unable to decode URL {}", url, e);
120                }
121                if (pdbId == null) {
122                        String path = url.getPath();
123                        pdbId = guessPDBID(path.substring(path.lastIndexOf("/") + 1));
124                }
125                return new SubstructureIdentifier((pdbId==null?(PdbId)null:new PdbId(pdbId)), ranges);
126        }
127
128        @Override
129        public Structure reduce(Structure input) throws StructureException {
130                return toCanonical().reduce(input);
131        }
132
133        /**
134         * Load the structure from the URL
135         * @return null
136         */
137        @Override
138        public Structure loadStructure(AtomCache cache) throws StructureException, IOException {
139                StructureFiletype format = StructureFiletype.UNKNOWN;
140
141                // Use user-specified format
142                try {
143                        Map<String, String> params = parseQuery(url);
144                        if (params.containsKey(FORMAT_PARAM)) {
145                                String formatStr = params.get(FORMAT_PARAM);
146                                format = StructureIO.guessFiletype("." + formatStr);
147                        }
148                } catch (UnsupportedEncodingException e) {
149                        logger.error("Unable to decode URL {}", url, e);
150                }
151
152                // Guess format from extension
153                if (format == StructureFiletype.UNKNOWN) {
154                        format = StructureIO.guessFiletype(url.getPath());
155                }
156
157                switch(format) {
158                        case CIF: case BCIF:
159                                return CifStructureConverter.fromURL(url, cache.getFileParsingParams());
160                        case MMTF:
161                                return MmtfActions.readFromInputStream(url.openStream());
162                        default: case PDB:
163                                // pdb file based parsing
164                                PDBFileReader reader = new PDBFileReader(cache.getPath());
165                                reader.setFetchBehavior(cache.getFetchBehavior());
166                                reader.setObsoleteBehavior(cache.getObsoleteBehavior());
167                                reader.setFileParsingParameters(cache.getFileParsingParams());
168                                return reader.getStructure(url);
169                }
170        }
171
172        /**
173         * Recognizes PDB IDs that occur at the beginning of name followed by some
174         * delimiter.
175         * @param name Input filename
176         * @return A 4-character id-like string, or null if none is found
177         */
178        public static String guessPDBID(String name) {
179                Matcher match = PDBID_REGEX.matcher(name);
180                if (match.matches()) {
181                        return match.group(1).toUpperCase();
182                }
183                // Give up if doesn't match
184                return null;
185        }
186
187        /**
188         * Parses URL parameters into a map. Keys are stored lower-case.
189         *
190         * @param url
191         * @return
192         * @throws UnsupportedEncodingException
193         */
194        private static Map<String,String> parseQuery(URL url) throws UnsupportedEncodingException {
195                Map<String,String> params = new LinkedHashMap<>();
196                String query = url.getQuery();
197                if (query == null || query.isEmpty()) {
198                        // empty query
199                        return params;
200                }
201                String[] pairs = url.getQuery().split("&");
202                for (String pair : pairs) {
203                        int i = pair.indexOf("=");
204                        String key = pair;
205                        if (i > 0) {
206                                key = URLDecoder.decode(pair.substring(0, i), "UTF-8");
207                        }
208                        String value = null;
209                        if(i > 0 && pair.length() > i + 1) {
210                                value = URLDecoder.decode(pair.substring(i + 1), "UTF-8");
211                        }
212                        // note that this uses the last instance if a parameter is specified multiple times
213                        params.put(key.toLowerCase(), value);
214                }
215                return params;
216        }
217}