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