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 at Oct 18, 2008
021 */
022package org.biojava.nbio.structure.io;
023
024import java.io.File;
025import java.io.IOException;
026import java.io.InputStream;
027import java.net.URL;
028import java.nio.file.Files;
029import java.text.ParseException;
030import java.text.SimpleDateFormat;
031import java.util.ArrayList;
032import java.util.Collections;
033import java.util.Date;
034import java.util.LinkedList;
035import java.util.List;
036import java.util.Locale;
037
038import org.biojava.nbio.core.util.FileDownloadUtils;
039import org.biojava.nbio.core.util.InputStreamProvider;
040import org.biojava.nbio.structure.PdbId;
041import org.biojava.nbio.structure.PDBStatus;
042import org.biojava.nbio.structure.PDBStatus.Status;
043import org.biojava.nbio.structure.Structure;
044import org.biojava.nbio.structure.StructureException;
045import org.biojava.nbio.structure.align.util.UserConfiguration;
046import org.rcsb.mmtf.utils.CodecUtils;
047import org.slf4j.Logger;
048import org.slf4j.LoggerFactory;
049
050/**
051 * Superclass for classes which download and interact with the PDB's FTP server,
052 * specifically {@link PDBFileReader} and {@link CifFileReader}. The basic
053 * functionality of downloading structure files from the FTP site is gathered
054 * here, making the child classes responsible for only the specific paths and
055 * file formats needed.
056 *
057 * @author Spencer Bliven
058 *
059 */
060public abstract class LocalPDBDirectory implements StructureIOFile {
061
062        private static final Logger logger = LoggerFactory.getLogger(LocalPDBDirectory.class);
063
064        /**
065         * The default server name, prefixed by the protocol string (http://, https:// or ftp://).
066         * Note that we don't support file stamp retrieving for ftp protocol, thus some of the
067         * fetch modes will not work properly with ftp protocol
068         */
069        public static final String DEFAULT_PDB_FILE_SERVER = "https://ftp.wwpdb.org";
070        public static final String PDB_FILE_SERVER_PROPERTY = "PDB.FILE.SERVER";
071
072        /**
073         * The default server to retrieve BinaryCIF files.
074         */
075        public static final String DEFAULT_BCIF_FILE_SERVER = "https://models.rcsb.org/";
076
077        /**
078         * Behaviors for when an obsolete structure is requested.
079         * @author Spencer Bliven
080         * @see LocalPDBDirectory#setObsoleteBehavior(ObsoleteBehavior)
081         */
082        public static enum ObsoleteBehavior {
083                /** Fetch the most recent version of the PDB entry. */
084                FETCH_CURRENT,
085                /** Fetch the obsolete entry from the PDB archives. */
086                FETCH_OBSOLETE,
087                /** Throw a StructureException for obsolete entries.*/
088                THROW_EXCEPTION;
089
090                public static final ObsoleteBehavior DEFAULT=THROW_EXCEPTION;
091        }
092
093        /**
094         * Controls when the class should fetch files from the ftp server
095         * @author Spencer Bliven
096         *
097         */
098        public static enum FetchBehavior {
099                /** Never fetch from the server; Throw errors for missing files */
100                LOCAL_ONLY,
101                /** Fetch missing files from the server. Don't check for outdated files */
102                FETCH_FILES,
103                /**
104                 * Fetch missing files from the server, also fetch if file present but older than the
105                 * server file.
106                 * This requires always querying the server for the last modified time of the file, thus
107                 * it adds an overhead to getting files from cache.
108                 */
109                FETCH_IF_OUTDATED,
110                /**
111                 * Fetch missing files from the server.
112                 * Also force the download of files older than {@value #LAST_REMEDIATION_DATE_STRING}.
113                 */
114                FETCH_REMEDIATED,
115                /** For every file, force downloading from the server */
116                FORCE_DOWNLOAD;
117
118                public static final FetchBehavior DEFAULT = FETCH_REMEDIATED;
119        }
120
121        /**
122         * Date of the latest PDB file remediation
123         */
124        public static final long LAST_REMEDIATION_DATE ;
125        private static final String LAST_REMEDIATION_DATE_STRING = "2011/07/12";
126        
127        static {
128
129                SimpleDateFormat formatter = new SimpleDateFormat("yyyy/MM/dd", Locale.US);
130
131                long t = 0;
132                try {
133                        Date d = formatter.parse(LAST_REMEDIATION_DATE_STRING);
134                        t = d.getTime();
135                } catch (ParseException e){
136                        logger.error("Unexpected error! could not parse LAST_REMEDIATION_DATE: "+e.getMessage());
137                }
138                LAST_REMEDIATION_DATE = t;
139        }
140
141        protected static final String lineSplit = System.getProperty("file.separator");
142
143        /** Minimum size for a valid structure file (CIF or PDB), in bytes */
144        public static final long MIN_PDB_FILE_SIZE = 40;  // Empty gzip files are 20bytes. Add a few more for buffer.
145
146        private File path;
147        private List<String> extensions;
148
149        /**
150         * The server name, prefixed by the protocol string (http:// or ftp://).
151         * Note that we don't support file stamp retrieving for ftp protocol, thus some of the
152         * fetch modes will not work properly with ftp protocol
153         */
154        private String serverName;
155
156        private FileParsingParameters params;
157
158        private ObsoleteBehavior obsoleteBehavior;
159        private FetchBehavior fetchBehavior;
160
161
162        // Cache results of get*DirPath()
163        private String splitDirURL; // path on the server, starting with a slash and ending before the 2-char split directories
164        private String obsoleteDirURL;
165        private File splitDirPath; // path to the directory before the 2-char split
166        private File obsoleteDirPath;
167
168        /**
169         * Subclasses should provide default and single-string constructors.
170         * They should use {@link #addExtension(String)} to add one or more extensions.
171         *
172         * <p>If path is null, initialize using the system property/environment variable
173         * {@link UserConfiguration#PDB_DIR}.
174         * @param path Path to the PDB file directory
175         */
176        public LocalPDBDirectory(String path) {
177                extensions    = new ArrayList<String>();
178
179                params = new FileParsingParameters();
180
181                if( path == null) {
182                        UserConfiguration config = new UserConfiguration();
183                        path = config.getPdbFilePath();
184                        logger.debug("Initialising from system property/environment variable to path: {}", path);
185                } else {
186                        path = FileDownloadUtils.expandUserHome(path);
187                        logger.debug("Initialising with path {}", path);
188                }
189                this.path = new File(path);
190
191                this.serverName = getServerName();
192
193                // Initialize splitDirURL,obsoleteDirURL,splitDirPath,obsoleteDirPath
194                initPaths();
195
196                fetchBehavior = FetchBehavior.DEFAULT;
197                obsoleteBehavior = ObsoleteBehavior.DEFAULT;
198        }
199
200        public LocalPDBDirectory() {
201                this(null);
202        }
203
204        /**
205         * Sets the path for the directory where PDB files are read/written
206         */
207        public void setPath(String p){
208                path = new File(FileDownloadUtils.expandUserHome(p)) ;
209                initPaths();
210        }
211
212        /**
213         * Returns the path value.
214         * @return a String representing the path value
215         * @see #setPath
216         *
217         */
218        public String getPath() {
219                return path.toString() ;
220        }
221
222        /** define supported file extensions
223         * compressed extensions .Z,.gz do not need to be specified
224         * they are dealt with automatically.
225         */
226        @Override
227        public void addExtension(String s){
228                //System.out.println("add Extension "+s);
229                extensions.add(s);
230        }
231
232        @Override
233        public List<String> getExtensions() {
234                return Collections.unmodifiableList(extensions);
235        }
236
237        /** clear the supported file extensions
238         *
239         */
240        public void clearExtensions(){
241                extensions.clear();
242        }
243
244        @Override
245        public void setFileParsingParameters(FileParsingParameters params){
246                this.params= params;
247        }
248
249        @Override
250        public FileParsingParameters getFileParsingParameters(){
251                return params;
252        }
253
254        /**
255         * <b>[Optional]</b> This method changes the behavior when obsolete entries
256         * are requested. Current behaviors are:
257         * <ul>
258         * <li>{@link ObsoleteBehavior#THROW_EXCEPTION THROW_EXCEPTION}
259         *   Throw a {@link StructureException} (the default)
260         * <li>{@link ObsoleteBehavior#FETCH_OBSOLETE FETCH_OBSOLETE}
261         *   Load the requested ID from the PDB's obsolete repository
262         * <li>{@link ObsoleteBehavior#FETCH_CURRENT FETCH_CURRENT}
263         *   Load the most recent version of the requested structure
264         *
265         * <p>This setting may be silently ignored by implementations which do not have
266         * access to the server to determine whether an entry is obsolete, such as
267         * if {@link #isAutoFetch()} is false. Note that an obsolete entry may still be
268         * returned even this is FETCH_CURRENT if the entry is found locally.
269         *
270         * @param fetchFileEvenIfObsolete Whether to fetch obsolete records
271         * @see #setFetchCurrent(boolean)
272         * @since 4.0.0
273         */
274        public void setObsoleteBehavior(ObsoleteBehavior behavior) {
275                obsoleteBehavior = behavior;
276        }
277
278        /**
279         * Returns how this instance deals with obsolete entries. Note that this
280         * setting may be ignored by some implementations or in some situations,
281         * such as when {@link #isAutoFetch()} is false.
282         *
283         * <p>For most implementations, the default value is
284         * {@link ObsoleteBehavior#THROW_EXCEPTION THROW_EXCEPTION}.
285         *
286         * @return The ObsoleteBehavior
287         * @since 4.0.0
288         */
289        public ObsoleteBehavior getObsoleteBehavior() {
290                return obsoleteBehavior;
291        }
292
293        /**
294         * Get the behavior for fetching files from the server
295         * @return
296         */
297        public FetchBehavior getFetchBehavior() {
298                return fetchBehavior;
299        }
300        /**
301         * Set the behavior for fetching files from the server.
302         * This replaces the {@link #setAutoFetch(boolean)} method with a more
303         * extensive set of options.
304         * @param fetchBehavior
305         */
306        public void setFetchBehavior(FetchBehavior fetchBehavior) {
307                this.fetchBehavior = fetchBehavior;
308        }
309
310
311        @Override
312        public Structure getStructure(String filename) throws IOException
313        {
314                filename = FileDownloadUtils.expandUserHome(filename);
315                File f = new File(filename);
316                return getStructure(f);
317
318        }
319
320        public Structure getStructure(URL u) throws IOException{
321                InputStreamProvider isp = new InputStreamProvider();
322                InputStream inStream = isp.getInputStream(u);
323                return getStructure(inStream);
324        }
325
326        @Override
327        public Structure getStructure(File filename) throws IOException {
328                InputStreamProvider isp = new InputStreamProvider();
329
330                InputStream inStream = isp.getInputStream(filename);
331
332                return getStructure(inStream);
333        }
334
335
336        /**
337         *{@inheritDoc}
338         */
339        public Structure getStructureById(String pdbId) throws IOException {
340                return getStructureById(new PdbId(pdbId));
341        }
342
343        /**
344         *{@inheritDoc}
345         */
346        @Override
347        public Structure getStructureById(PdbId pdbId) throws IOException {
348                InputStream inStream = getInputStream(pdbId);
349                return getStructure(inStream);
350        }
351
352        /**
353         * Handles the actual parsing of the file into a Structure object.
354         * @param inStream
355         * @return
356         * @throws IOException
357         */
358        public abstract Structure getStructure(InputStream inStream) throws IOException;
359
360        /**
361         * Load or download the specified structure and return it as an InputStream
362         * for direct parsing.
363         * @param pdbId
364         * @return
365         * @throws IOException
366         */
367        protected InputStream getInputStream(PdbId pdbId) throws IOException{
368
369                // Check existing
370                File file = downloadStructure(pdbId);
371
372                if(!file.exists()) {
373                        throw new IOException("Structure "+pdbId+" not found and unable to download.");
374                }
375
376                InputStreamProvider isp = new InputStreamProvider();
377
378                InputStream inputStream = isp.getInputStream(file);
379
380                return inputStream;
381        }
382
383        /**
384         * Download a structure, but don't parse it yet or store it in memory.
385         *
386         * Used to pre-fetch large numbers of structures.
387         * @param pdbId
388         * @throws IOException
389         */
390        public void prefetchStructure(String pdbId) throws IOException {
391                
392                // Check existing
393                File file = downloadStructure(new PdbId(pdbId));
394
395                if(!file.exists()) {
396                        throw new IOException("Structure "+pdbId+" not found and unable to download.");
397                }
398        }
399
400        /**
401         * Attempts to delete all versions of a structure from the local directory.
402         * @param pdbId a String representing the PDB ID.
403         * @return True if one or more files were deleted
404         * @throws IOException if the file cannot be deleted
405         */
406        public boolean deleteStructure(String pdbId) throws IOException {
407                return deleteStructure(new PdbId(pdbId));
408        }
409
410        /**
411         * Attempts to delete all versions of a structure from the local directory.
412         * @param pdbId The PDB ID
413         * @return True if one or more files were deleted
414         * @throws IOException if the file cannot be deleted
415         */
416        public boolean deleteStructure(PdbId pdbId) throws IOException{
417                boolean deleted = false;
418                // Force getLocalFile to check in obsolete locations
419                ObsoleteBehavior obsolete = getObsoleteBehavior();
420                setObsoleteBehavior(ObsoleteBehavior.FETCH_OBSOLETE);
421
422                try {
423                        File existing = getLocalFile(pdbId);
424                        while(existing != null) {
425                                assert(existing.exists()); // should exist unless concurrency problems
426
427                                if( getFetchBehavior() == FetchBehavior.LOCAL_ONLY) {
428                                        throw new RuntimeException("Refusing to delete from LOCAL_ONLY directory");
429                                }
430
431                                // delete file
432                                boolean success = existing.delete();
433                                if(success) {
434                                        logger.debug("Deleting "+existing.getAbsolutePath());
435                                }
436                                deleted = deleted || success;
437
438                                // delete parent if empty
439                                File parent = existing.getParentFile();
440                                if(parent != null) {
441                                        success = parent.delete();
442                                        if(success) {
443                                                logger.debug("Deleting "+parent.getAbsolutePath());
444                                        }
445                                }
446
447                                existing = getLocalFile(pdbId);
448                        }
449                        return deleted;
450
451                } finally {
452                        setObsoleteBehavior(obsolete);
453                }
454        }
455
456        /**
457         * Downloads an MMCIF file from the PDB to the local path
458         * @param pdbId
459         * @return The file, or null if it was unavailable for download
460         * @throws IOException for errors downloading or writing, or if the
461         *  fetchBehavior is {@link FetchBehavior#LOCAL_ONLY}
462         */
463        protected File downloadStructure(PdbId pdbId) throws IOException {
464
465                // decide whether download is required
466                File existing =  getLocalFile(pdbId);
467                switch(fetchBehavior) {
468                case LOCAL_ONLY:
469                        if( existing == null ) {
470                                throw new IOException(String.format("Structure %s not found in %s "
471                                                + "and configured not to download.",pdbId,getPath()));
472                        } else {
473                                return existing;
474                        }
475                case FETCH_FILES:
476                        // Use existing if present
477                        if( existing != null) {
478                                return existing;
479                        }
480                        // existing is null, downloadStructure(String,String,boolean,File) will download it
481                        break;
482                case FETCH_IF_OUTDATED:
483                        // here existing can be null or not:
484                        // existing == null : downloadStructure(String,String,boolean,File) will download it
485                        // existing != null : downloadStructure(String,String,boolean,File) will check its date and download if older
486                        break;
487                case FETCH_REMEDIATED:
488                        // Use existing if present and recent enough
489                        if( existing != null) {
490                                long lastModified = existing.lastModified();
491
492                                if (lastModified < LAST_REMEDIATION_DATE) {
493                                        // the file is too old, replace with newer version
494                                        logger.warn("Replacing file {} with latest remediated (remediation of {}) file from PDB.",
495                                                        existing, LAST_REMEDIATION_DATE_STRING);
496                                        existing = null;
497                                        break;
498                                } else {
499                                        return existing;
500                                }
501                        }
502                case FORCE_DOWNLOAD:
503                        // discard the existing file to force redownload
504                        existing = null; // downloadStructure(String,String,boolean,File) will download it
505                        break;
506                }
507
508                // Force the download now
509                if(obsoleteBehavior == ObsoleteBehavior.FETCH_CURRENT) {
510                        String current = PDBStatus.getCurrent(pdbId.getId());
511                        PdbId pdbIdToDownload = null;
512                        if(current == null) {
513                                // either an error or there is not current entry
514                                pdbIdToDownload = pdbId;
515                        }else {
516                                pdbIdToDownload = new PdbId(current);
517                        }
518                        return downloadStructure(pdbIdToDownload, splitDirURL,false, existing);
519                } else if(obsoleteBehavior == ObsoleteBehavior.FETCH_OBSOLETE
520                                && PDBStatus.getStatus(pdbId.getId()) == Status.REMOVED) {
521                        return downloadStructure(pdbId, obsoleteDirURL, true, existing);
522                } else {
523                        return downloadStructure(pdbId, splitDirURL, false, existing);
524                }
525        }
526
527        /**
528         * Download a file from the ftp server, replacing any existing files if needed
529         * @param pdbId PDB ID
530         * @param pathOnServer Path on the FTP server, e.g. data/structures/divided/pdb
531         * @param obsolete Whether or not file should be saved to the obsolete location locally
532         * @param existingFile if not null and checkServerFileDate is true, the last modified date of the
533         * server file and this file will be compared to decide whether to download or not
534         * @return
535         * @throws IOException
536         */
537        private File downloadStructure(PdbId pdbId, String pathOnServer, boolean obsolete, File existingFile)
538                        throws IOException{
539                String id = pdbId.getId().toLowerCase();
540                File dir = getDir(id, obsolete);
541                File realFile = new File(dir,getFilename(id));
542
543                String ftp;
544
545                String filename = getFilename(id);
546                if (filename.endsWith(".mmtf.gz")){
547                        ftp = CodecUtils.getMmtfEntryUrl(id, true, false);
548                } else if (filename.endsWith(".bcif") || filename.endsWith(".bcif.gz")) {
549                        // TODO this should be configurable
550                        ftp = DEFAULT_BCIF_FILE_SERVER + filename;
551                } else {
552                        ftp = String.format("%s%s/%s/%s",
553                        serverName, pathOnServer, id.substring(id.length()-3, id.length()-1), getFilename(id));
554                }
555
556                URL url = new URL(ftp);
557
558                Date serverFileDate = null;
559                if (existingFile!=null) {
560
561                        serverFileDate = getLastModifiedTime(url);
562
563                        if (serverFileDate!=null) {
564                                if (existingFile.lastModified()>=serverFileDate.getTime()) {
565                                        return existingFile;
566                                } else {
567                                        // otherwise we go ahead and download, warning about it first
568                                        logger.warn("File {} is outdated, will download new one from PDB (updated on {})",
569                                                        existingFile, serverFileDate.toString());
570                                }
571                        } else {
572                                logger.warn("Could not determine if file {} is outdated (could not get timestamp from server). Will force redownload", existingFile);
573                        }
574                }
575
576                logger.info("Fetching " + ftp);
577                logger.info("Writing to "+ realFile);
578
579                FileDownloadUtils.downloadFile(url, realFile);
580
581                return realFile;
582        }
583
584        /**
585         * Get the last modified time of the file in given url by retrieveing the "Last-Modified" header.
586         * Note that this only works for http URLs
587         * @param url
588         * @return the last modified date or null if it couldn't be retrieved (in that case a warning will be logged)
589         */
590        private Date getLastModifiedTime(URL url) {
591
592                // see http://stackoverflow.com/questions/2416872/how-do-you-obtain-modified-date-from-a-remote-file-java
593                Date date = null;
594                try {
595                        String lastModified = url.openConnection().getHeaderField("Last-Modified");
596                        logger.debug("Last modified date of server file ({}) is {}",url.toString(),lastModified);
597
598
599                        if (lastModified!=null) {
600
601                                try {
602                                        date = new SimpleDateFormat("E, d MMM yyyy HH:mm:ss Z", Locale.ENGLISH).parse(lastModified);
603                                } catch (ParseException e) {
604                                        logger.warn("Could not parse last modified time from string '{}', no last modified time available for file {}",
605                                                        lastModified, url.toString());
606                                        // this will return null
607                                }
608
609                        }
610                } catch (IOException e) {
611                        logger.warn("Problems while retrieving last modified time for file {}", url.toString());
612                }
613                return date;
614
615        }
616
617        /**
618         * Gets the directory in which the file for a given MMCIF file would live,
619         * creating it if necessary.
620         *
621         * The obsolete parameter is necessary to avoid additional server queries.
622         * @param pdbId
623         * @param obsolete Whether the pdbId is obsolete or not
624         * @return File pointing to the directory,
625         */
626        protected File getDir(String pdbId, boolean obsolete) {
627
628                File dir = null;
629                int offset = pdbId.length() - 3;
630
631                if (obsolete) {
632                        // obsolete is always split
633                        String middle = pdbId.substring(offset, offset + 2).toLowerCase();
634                        dir = new File(obsoleteDirPath, middle);
635                } else {
636                        String middle = pdbId.substring(offset, offset + 2).toLowerCase();
637                        dir = new File(splitDirPath, middle);
638                }
639
640
641                if (!dir.exists()) {
642                        boolean success = dir.mkdirs();
643                        if (!success) logger.error("Could not create mmCIF dir {}",dir.toString());
644                }
645
646                return dir;
647        }
648
649        /**
650         * Searches for previously downloaded files
651         * @param pdbId
652         * @return A file pointing to the existing file, or null if not found
653         * @throws IOException If the file exists but is empty and can't be deleted
654         */
655        public File getLocalFile(String pdbId) throws IOException {
656                return getLocalFile(new PdbId(pdbId));
657        }
658        /**
659         * Searches for previously downloaded files
660         * @param pdbId
661         * @return A file pointing to the existing file, or null if not found
662         * @throws IOException If the file exists but is empty and can't be deleted
663         */
664        public File getLocalFile(PdbId pdbId) throws IOException {
665                String id = pdbId.getId();
666                int offset = id.length() - 3;
667                // Search for existing files
668
669                // Search directories:
670                // 1) LOCAL_MMCIF_SPLIT_DIR/<middle>/(pdb)?<pdbId>.<ext>
671                // 2) LOCAL_MMCIF_ALL_DIR/<middle>/(pdb)?<pdbId>.<ext>
672                LinkedList<File> searchdirs = new LinkedList<File>();
673                String middle = id.substring(offset, offset+2).toLowerCase();
674
675                File splitdir = new File(splitDirPath, middle);
676                searchdirs.add(splitdir);
677                // Search obsolete files if requested
678                if(getObsoleteBehavior() == ObsoleteBehavior.FETCH_OBSOLETE) {
679                        File obsdir = new File(obsoleteDirPath,middle);
680                        searchdirs.add(obsdir);
681                }
682
683                // valid prefixes before the <pdbId> in the filename
684                String[] prefixes = new String[] {"", "pdb"};
685
686                for( File searchdir :searchdirs){
687                        for( String prefix : prefixes) {
688                                for(String ex : getExtensions() ){
689                                        File f = new File(searchdir,prefix + id.toLowerCase() + ex) ;
690                                        if ( f.exists()) {
691                                                // delete files that are too short to have contents
692                                                if( f.length() < MIN_PDB_FILE_SIZE ) {
693                                                        Files.delete(f.toPath());
694                                                        return null;
695                                                }
696                                                return f;
697                                        }
698                                }
699                        }
700                }
701                //Not found
702                return null;
703        }
704
705        protected boolean checkFileExists(String pdbId) {
706                return checkFileExists(new PdbId(pdbId));
707        }
708        protected boolean checkFileExists(PdbId pdbId){
709                try {
710                        File path =  getLocalFile(pdbId);
711                        if ( path != null)
712                                return true;
713                } catch(IOException e) {}
714                return false;
715        }
716
717        /**
718         * Return the String with the PDB server name, including the leading protocol
719         * String (http:// or ftp://).
720         * The server name will be by default the value {@value #DEFAULT_PDB_FILE_SERVER} or the one
721         * read from system property {@value #PDB_FILE_SERVER_PROPERTY}
722         *
723         * @return the server name including the leading protocol string
724         */
725        public static String getServerName() {
726                String name = System.getProperty(PDB_FILE_SERVER_PROPERTY);
727
728                if ( name == null || name.trim().isEmpty()) {
729                        name = DEFAULT_PDB_FILE_SERVER;
730                        logger.debug("Using default PDB file server {}",name);
731                } else {
732                        if (!name.startsWith("http://") && !name.startsWith("ftp://") && !name.startsWith("https://")) {
733                                logger.warn("Server name {} read from system property {} does not have a leading protocol string. Adding http:// to it", name, PDB_FILE_SERVER_PROPERTY);
734                                name = "http://"+name;
735                        }
736                        logger.info("Using PDB file server {} read from system property {}", name, PDB_FILE_SERVER_PROPERTY);
737                }
738                return name;
739        }
740
741        /**
742         * Should be called whenever any of the path variables change.
743         * Thus, if {@link getSplitDirPath()} or {@link getObsoleteDirPath()}
744         * depend on anything, they should call this function when that thing
745         * changes (possibly including at the end of the constructor).
746         */
747        protected void initPaths() {
748                // Hand-rolled String.join(), for java 6
749                String[] split = getSplitDirPath();
750                String[] obsolete = getObsoleteDirPath();
751
752                //URLs are joined with '/'
753                StringBuilder splitURL = new StringBuilder("/pub/pdb");
754                for(int i=0;i<split.length;i++) {
755                        splitURL.append("/");
756                        splitURL.append(split[i]);
757                }
758                StringBuilder obsoleteURL = new StringBuilder("/pub/pdb");
759                for(int i=0;i<obsolete.length;i++) {
760                        obsoleteURL.append("/");
761                        obsoleteURL.append(obsolete[i]);
762                }
763
764                splitDirURL = splitURL.toString();
765                obsoleteDirURL = obsoleteURL.toString();
766
767
768                //Files join themselves iteratively
769                splitDirPath = path;
770                for(int i=0;i<split.length;i++) {
771                        splitDirPath = new File(splitDirPath,split[i]);
772                }
773                obsoleteDirPath = path;
774                for(int i=0;i<obsolete.length;i++) {
775                        obsoleteDirPath = new File(obsoleteDirPath,obsolete[i]);
776                }
777        }
778
779        /**
780         * Converts a PDB ID into a filename with the proper extension
781         * @param pdbId
782         * @return The filename, e.g. "4hhb.pdb.gz"
783         */
784        protected abstract String getFilename(String pdbId);
785
786        /**
787         * Location of split files within the directory, as an array of paths.
788         * These will be joined with either slashes (for the URL) or the file
789         * separator (for directories). The returned results should be constant,
790         * to allow for caching.
791         * @return A list of directories, relative to the /pub/pdb directory on the server
792         */
793        protected abstract String[] getSplitDirPath();
794        /**
795         * Location of obsolete files within the directory, as an array of paths.
796         * These will be joined with either slashes (for the URL) or the file
797         * separator (for directories). The returned results should be constant,
798         * to allow for caching.
799         * @return A list of directories, relative to the /pub/pdb directory on the server
800         */
801        protected abstract String[] getObsoleteDirPath();
802}