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://files.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        /** Minimum size for a valid structure file (CIF or PDB), in bytes */
142        public static final long MIN_PDB_FILE_SIZE = 40;  // Empty gzip files are 20bytes. Add a few more for buffer.
143
144        private File path;
145        private List<String> extensions;
146
147        /**
148         * The server name, prefixed by the protocol string (http:// or ftp://).
149         * Note that we don't support file stamp retrieving for ftp protocol, thus some of the
150         * fetch modes will not work properly with ftp protocol
151         */
152        private String serverName;
153
154        private FileParsingParameters params;
155
156        private ObsoleteBehavior obsoleteBehavior;
157        private FetchBehavior fetchBehavior;
158
159
160        // Cache results of get*DirPath()
161        private String splitDirURL; // path on the server, starting with a slash and ending before the 2-char split directories
162        private String obsoleteDirURL;
163        private File splitDirPath; // path to the directory before the 2-char split
164        private File obsoleteDirPath;
165
166        /**
167         * Subclasses should provide default and single-string constructors.
168         * They should use {@link #addExtension(String)} to add one or more extensions.
169         *
170         * <p>If path is null, initialize using the system property/environment variable
171         * {@link UserConfiguration#PDB_DIR}.
172         * @param path Path to the PDB file directory
173         */
174        public LocalPDBDirectory(String path) {
175                extensions    = new ArrayList<>();
176
177                params = new FileParsingParameters();
178
179                if( path == null) {
180                        UserConfiguration config = new UserConfiguration();
181                        path = config.getPdbFilePath();
182                        logger.debug("Initialising from system property/environment variable to path: {}", path);
183                } else {
184                        path = FileDownloadUtils.expandUserHome(path);
185                        logger.debug("Initialising with path {}", path);
186                }
187                this.path = new File(path);
188
189                this.serverName = getServerName();
190
191                // Initialize splitDirURL,obsoleteDirURL,splitDirPath,obsoleteDirPath
192                initPaths();
193
194                fetchBehavior = FetchBehavior.DEFAULT;
195                obsoleteBehavior = ObsoleteBehavior.DEFAULT;
196        }
197
198        public LocalPDBDirectory() {
199                this(null);
200        }
201
202        /**
203         * Sets the path for the directory where PDB files are read/written
204         */
205        public void setPath(String p){
206                path = new File(FileDownloadUtils.expandUserHome(p)) ;
207                initPaths();
208        }
209
210        /**
211         * Returns the path value.
212         * @return a String representing the path value
213         * @see #setPath
214         *
215         */
216        public String getPath() {
217                return path.toString() ;
218        }
219
220        /** define supported file extensions
221         * compressed extensions .Z,.gz do not need to be specified
222         * they are dealt with automatically.
223         */
224        @Override
225        public void addExtension(String s){
226                //System.out.println("add Extension "+s);
227                extensions.add(s);
228        }
229
230        @Override
231        public List<String> getExtensions() {
232                return Collections.unmodifiableList(extensions);
233        }
234
235        /** clear the supported file extensions
236         *
237         */
238        public void clearExtensions(){
239                extensions.clear();
240        }
241
242        @Override
243        public void setFileParsingParameters(FileParsingParameters params){
244                this.params= params;
245        }
246
247        @Override
248        public FileParsingParameters getFileParsingParameters(){
249                return params;
250        }
251
252        /**
253         * <b>[Optional]</b> This method changes the behavior when obsolete entries
254         * are requested. Current behaviors are:
255         * <ul>
256         * <li>{@link ObsoleteBehavior#THROW_EXCEPTION THROW_EXCEPTION}
257         *   Throw a {@link StructureException} (the default)
258         * <li>{@link ObsoleteBehavior#FETCH_OBSOLETE FETCH_OBSOLETE}
259         *   Load the requested ID from the PDB's obsolete repository
260         * <li>{@link ObsoleteBehavior#FETCH_CURRENT FETCH_CURRENT}
261         *   Load the most recent version of the requested structure
262         * </ul>
263         * <p>This setting may be silently ignored by implementations which do not have
264         * access to the server to determine whether an entry is obsolete, such as
265         * certain {@link FetchBehavior}s. Note that an obsolete entry may still be
266         * returned even this is FETCH_CURRENT if the entry is found locally.
267         *
268         * @param behavior Whether to fetch obsolete records
269         * @see #setFetchBehavior(FetchBehavior)
270         * @since 4.0.0
271         */
272        public void setObsoleteBehavior(ObsoleteBehavior behavior) {
273                obsoleteBehavior = behavior;
274        }
275
276        /**
277         * Returns how this instance deals with obsolete entries. Note that this
278         * setting may be ignored by some implementations or in some situations,
279         * such as certain {@link FetchBehavior}s.
280         *
281         * <p>For most implementations, the default value is
282         * {@link ObsoleteBehavior#THROW_EXCEPTION THROW_EXCEPTION}.
283         *
284         * @return The ObsoleteBehavior
285         * @since 4.0.0
286         */
287        public ObsoleteBehavior getObsoleteBehavior() {
288                return obsoleteBehavior;
289        }
290
291        /**
292         * Get the behavior for fetching files from the server
293         * @return
294         */
295        public FetchBehavior getFetchBehavior() {
296                return fetchBehavior;
297        }
298
299        /**
300         * Set the behavior for fetching files from the server.
301         * @param fetchBehavior
302         */
303        public void setFetchBehavior(FetchBehavior fetchBehavior) {
304                this.fetchBehavior = fetchBehavior;
305        }
306
307
308        @Override
309        public Structure getStructure(String filename) throws IOException
310        {
311                filename = FileDownloadUtils.expandUserHome(filename);
312                File f = new File(filename);
313                return getStructure(f);
314
315        }
316
317        public Structure getStructure(URL u) throws IOException{
318                InputStreamProvider isp = new InputStreamProvider();
319                InputStream inStream = isp.getInputStream(u);
320                return getStructure(inStream);
321        }
322
323        @Override
324        public Structure getStructure(File filename) throws IOException {
325                InputStreamProvider isp = new InputStreamProvider();
326
327                InputStream inStream = isp.getInputStream(filename);
328
329                return getStructure(inStream);
330        }
331
332
333        /**
334         *{@inheritDoc}
335         */
336        public Structure getStructureById(String pdbId) throws IOException {
337                return getStructureById(new PdbId(pdbId));
338        }
339
340        /**
341         *{@inheritDoc}
342         */
343        @Override
344        public Structure getStructureById(PdbId pdbId) throws IOException {
345                InputStream inStream = getInputStream(pdbId);
346                return getStructure(inStream);
347        }
348
349        /**
350         * Handles the actual parsing of the file into a Structure object.
351         * @param inStream
352         * @return
353         * @throws IOException
354         */
355        public abstract Structure getStructure(InputStream inStream) throws IOException;
356
357        /**
358         * Load or download the specified structure and return it as an InputStream
359         * for direct parsing.
360         * @param pdbId
361         * @return
362         * @throws IOException in cases of file I/O, including failure to download a healthy (non-corrupted) file.
363         */
364        protected InputStream getInputStream(PdbId pdbId) throws IOException{
365
366                // Check existing
367                File file = downloadStructure(pdbId);
368
369                if(!file.exists()) {
370                        throw new IOException("Structure "+pdbId+" not found and unable to download.");
371                }
372
373                if(! FileDownloadUtils.validateFile(file))
374                        throw new IOException("Downloaded file invalid: "+file);
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 in cases of file I/O, including failure to download a healthy (non-corrupted) file.
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                if(! FileDownloadUtils.validateFile(file))
399                        throw new IOException("Downloaded file invalid: "+file);
400        }
401
402        /**
403         * Attempts to delete all versions of a structure from the local directory.
404         * @param pdbId a String representing the PDB ID.
405         * @return True if one or more files were deleted
406         * @throws IOException if the file cannot be deleted
407         */
408        public boolean deleteStructure(String pdbId) throws IOException {
409                return deleteStructure(new PdbId(pdbId));
410        }
411
412        /**
413         * Attempts to delete all versions of a structure from the local directory.
414         * @param pdbId The PDB ID
415         * @return True if one or more files were deleted
416         * @throws IOException if the file cannot be deleted
417         */
418        public boolean deleteStructure(PdbId pdbId) throws IOException{
419                boolean deleted = false;
420                // Force getLocalFile to check in obsolete locations
421                ObsoleteBehavior obsolete = getObsoleteBehavior();
422                setObsoleteBehavior(ObsoleteBehavior.FETCH_OBSOLETE);
423
424                try {
425                        File existing = getLocalFile(pdbId);
426                        while(existing != null) {
427                                assert(existing.exists()); // should exist unless concurrency problems
428
429                                if( getFetchBehavior() == FetchBehavior.LOCAL_ONLY) {
430                                        throw new RuntimeException("Refusing to delete from LOCAL_ONLY directory");
431                                }
432
433                                // delete file
434                                boolean success = existing.delete();
435                                if(success) {
436                                        logger.debug("Deleting {}", existing.getAbsolutePath());
437                                }
438                                deleted = deleted || success;
439
440                                // delete parent if empty
441                                File parent = existing.getParentFile();
442                                if(parent != null) {
443                                        success = parent.delete();
444                                        if(success) {
445                                                logger.debug("Deleting {}", parent.getAbsolutePath());
446                                        }
447                                }
448
449                                existing = getLocalFile(pdbId);
450                        }
451                        return deleted;
452
453                } finally {
454                        setObsoleteBehavior(obsolete);
455                }
456        }
457
458        /**
459         * Downloads an MMCIF file from the PDB to the local path
460         * @param pdbId
461         * @return The file, or null if it was unavailable for download
462         * @throws IOException for errors downloading or writing, or if the
463         *  fetchBehavior is {@link FetchBehavior#LOCAL_ONLY}
464         */
465        protected File downloadStructure(PdbId pdbId) throws IOException {
466
467                // decide whether download is required
468                File existing =  getLocalFile(pdbId);
469                switch(fetchBehavior) {
470                case LOCAL_ONLY:
471                        if( existing == null ) {
472                                throw new IOException(String.format("Structure %s not found in %s "
473                                                + "and configured not to download.",pdbId,getPath()));
474                        } else {
475                                return existing;
476                        }
477                case FETCH_FILES:
478                        // Use existing if present
479                        if( existing != null) {
480                                return existing;
481                        }
482                        // existing is null, downloadStructure(String,String,boolean,File) will download it
483                        break;
484                case FETCH_IF_OUTDATED:
485                        // here existing can be null or not:
486                        // existing == null : downloadStructure(String,String,boolean,File) will download it
487                        // existing != null : downloadStructure(String,String,boolean,File) will check its date and download if older
488                        break;
489                case FETCH_REMEDIATED:
490                        // Use existing if present and recent enough
491                        if( existing != null) {
492                                long lastModified = existing.lastModified();
493
494                                if (lastModified < LAST_REMEDIATION_DATE) {
495                                        // the file is too old, replace with newer version
496                                        logger.warn("Replacing file {} with latest remediated (remediation of {}) file from PDB.",
497                                                        existing, LAST_REMEDIATION_DATE_STRING);
498                                        existing = null;
499                                        break;
500                                } else {
501                                        return existing;
502                                }
503                        }
504                case FORCE_DOWNLOAD:
505                        // discard the existing file to force redownload
506                        existing = null; // downloadStructure(String,String,boolean,File) will download it
507                        break;
508                }
509
510                // Force the download now
511                if(obsoleteBehavior == ObsoleteBehavior.FETCH_CURRENT) {
512                        String current = PDBStatus.getCurrent(pdbId.getId());
513                        PdbId pdbIdToDownload = null;
514                        if(current == null) {
515                                // either an error or there is not current entry
516                                pdbIdToDownload = pdbId;
517                        }else {
518                                pdbIdToDownload = new PdbId(current);
519                        }
520                        return downloadStructure(pdbIdToDownload, splitDirURL,false, existing);
521                } else if(obsoleteBehavior == ObsoleteBehavior.FETCH_OBSOLETE
522                                && PDBStatus.getStatus(pdbId.getId()) == Status.REMOVED) {
523                        return downloadStructure(pdbId, obsoleteDirURL, true, existing);
524                } else {
525                        return downloadStructure(pdbId, splitDirURL, false, existing);
526                }
527        }
528
529        /**
530         * Download a file from the http server +/- its validation metadata, replacing any existing files if needed
531         * @param pdbId PDB ID
532         * @param pathOnServer Path on the http server, e.g. data/structures/divided/pdb
533         * @param obsolete Whether or not file should be saved to the obsolete location locally
534         * @param existingFile if not null and checkServerFileDate is true, the last modified date of the
535         * server file and this file will be compared to decide whether to download or not
536         * @return
537         * @throws IOException in cases of file I/O, including failure to download a healthy (non-corrupted) file.
538         */
539        private File downloadStructure(PdbId pdbId, String pathOnServer, boolean obsolete, File existingFile)
540                        throws IOException{
541                String id = pdbId.getId().toLowerCase();
542                File dir = getDir(id, obsolete);
543                File realFile = new File(dir,getFilename(id));
544
545                String ftp;
546
547                String filename = getFilename(id);
548                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.createValidationFiles(url, realFile, null, FileDownloadUtils.Hash.UNKNOWN);
580                FileDownloadUtils.downloadFile(url, realFile);
581                if(! FileDownloadUtils.validateFile(realFile))
582                        throw new IOException("Downloaded file invalid: "+realFile);
583
584                return realFile;
585        }
586
587        /**
588         * Get the last modified time of the file in given url by retrieveing the "Last-Modified" header.
589         * Note that this only works for http URLs
590         * @param url
591         * @return the last modified date or null if it couldn't be retrieved (in that case a warning will be logged)
592         */
593        private Date getLastModifiedTime(URL url) {
594
595                // see http://stackoverflow.com/questions/2416872/how-do-you-obtain-modified-date-from-a-remote-file-java
596                Date date = null;
597                try {
598                        String lastModified = url.openConnection().getHeaderField("Last-Modified");
599                        logger.debug("Last modified date of server file ({}) is {}",url.toString(),lastModified);
600
601
602                        if (lastModified!=null) {
603
604                                try {
605                                        date = new SimpleDateFormat("E, d MMM yyyy HH:mm:ss Z", Locale.ENGLISH).parse(lastModified);
606                                } catch (ParseException e) {
607                                        logger.warn("Could not parse last modified time from string '{}', no last modified time available for file {}",
608                                                        lastModified, url.toString());
609                                        // this will return null
610                                }
611
612                        }
613                } catch (IOException e) {
614                        logger.warn("Problems while retrieving last modified time for file {}", url.toString());
615                }
616                return date;
617
618        }
619
620        /**
621         * Gets the directory in which the file for a given MMCIF file would live,
622         * creating it if necessary.
623         *
624         * The obsolete parameter is necessary to avoid additional server queries.
625         * @param pdbId
626         * @param obsolete Whether the pdbId is obsolete or not
627         * @return File pointing to the directory,
628         */
629        protected File getDir(String pdbId, boolean obsolete) {
630
631                File dir = null;
632                int offset = pdbId.length() - 3;
633
634                if (obsolete) {
635                        // obsolete is always split
636                        String middle = pdbId.substring(offset, offset + 2).toLowerCase();
637                        dir = new File(obsoleteDirPath, middle);
638                } else {
639                        String middle = pdbId.substring(offset, offset + 2).toLowerCase();
640                        dir = new File(splitDirPath, middle);
641                }
642
643
644                if (!dir.exists()) {
645                        boolean success = dir.mkdirs();
646                        if (!success) logger.error("Could not create mmCIF dir {}",dir.toString());
647                }
648
649                return dir;
650        }
651
652        /**
653         * Searches for previously downloaded files
654         * @param pdbId
655         * @return A file pointing to the existing file, or null if not found
656         * @throws IOException If the file exists but is empty and can't be deleted
657         */
658        public File getLocalFile(String pdbId) throws IOException {
659                return getLocalFile(new PdbId(pdbId));
660        }
661        /**
662         * Searches for previously downloaded files
663         * @param pdbId
664         * @return A file pointing to the existing file, or null if not found
665         * @throws IOException If the file exists but is empty and can't be deleted
666         */
667        public File getLocalFile(PdbId pdbId) throws IOException {
668                String id = pdbId.getId();
669                int offset = id.length() - 3;
670                // Search for existing files
671
672                // Search directories:
673                // 1) LOCAL_MMCIF_SPLIT_DIR/<middle>/(pdb)?<pdbId>.<ext>
674                // 2) LOCAL_MMCIF_ALL_DIR/<middle>/(pdb)?<pdbId>.<ext>
675                LinkedList<File> searchdirs = new LinkedList<>();
676                String middle = id.substring(offset, offset+2).toLowerCase();
677
678                File splitdir = new File(splitDirPath, middle);
679                searchdirs.add(splitdir);
680                // Search obsolete files if requested
681                if(getObsoleteBehavior() == ObsoleteBehavior.FETCH_OBSOLETE) {
682                        File obsdir = new File(obsoleteDirPath,middle);
683                        searchdirs.add(obsdir);
684                }
685
686                // valid prefixes before the <pdbId> in the filename
687                String[] prefixes = new String[] {"", "pdb"};
688
689                for( File searchdir :searchdirs){
690                        for( String prefix : prefixes) {
691                                for(String ex : getExtensions() ){
692                                        File f = new File(searchdir,prefix + id.toLowerCase() + ex) ;
693                                        if ( f.exists()) {
694                                                // delete files that are too short to have contents
695                                                if( f.length() < MIN_PDB_FILE_SIZE ) {
696                                                        Files.delete(f.toPath());
697                                                        return null;
698                                                }
699                                                return f;
700                                        }
701                                }
702                        }
703                }
704                //Not found
705                return null;
706        }
707
708        protected boolean checkFileExists(String pdbId) {
709                return checkFileExists(new PdbId(pdbId));
710        }
711        protected boolean checkFileExists(PdbId pdbId){
712                try {
713                        File path =  getLocalFile(pdbId);
714                        if ( path != null)
715                                return true;
716                } catch(IOException e) {}
717                return false;
718        }
719
720        /**
721         * Return the String with the PDB server name, including the leading protocol
722         * String (http:// or ftp://).
723         * The server name will be by default the value {@value #DEFAULT_PDB_FILE_SERVER} or the one
724         * read from system property {@value #PDB_FILE_SERVER_PROPERTY}
725         *
726         * @return the server name including the leading protocol string
727         */
728        public static String getServerName() {
729                String name = System.getProperty(PDB_FILE_SERVER_PROPERTY);
730
731                if ( name == null || name.trim().isEmpty()) {
732                        name = DEFAULT_PDB_FILE_SERVER;
733                        logger.debug("Using default PDB file server {}",name);
734                } else {
735                        if (!name.startsWith("http://") && !name.startsWith("ftp://") && !name.startsWith("https://")) {
736                                logger.warn("Server name {} read from system property {} does not have a leading protocol string. Adding http:// to it", name, PDB_FILE_SERVER_PROPERTY);
737                                name = "http://"+name;
738                        }
739                        logger.info("Using PDB file server {} read from system property {}", name, PDB_FILE_SERVER_PROPERTY);
740                }
741                return name;
742        }
743
744        /**
745         * Should be called whenever any of the path variables change.
746         * Thus, if {@link getSplitDirPath()} or {@link getObsoleteDirPath()}
747         * depend on anything, they should call this function when that thing
748         * changes (possibly including at the end of the constructor).
749         */
750        protected void initPaths() {
751                // Hand-rolled String.join(), for java 6
752                String[] split = getSplitDirPath();
753                String[] obsolete = getObsoleteDirPath();
754
755                //URLs are joined with '/'
756                StringBuilder splitURL = new StringBuilder("/pub/pdb");
757                for(int i=0;i<split.length;i++) {
758                        splitURL.append("/");
759                        splitURL.append(split[i]);
760                }
761                StringBuilder obsoleteURL = new StringBuilder("/pub/pdb");
762                for(int i=0;i<obsolete.length;i++) {
763                        obsoleteURL.append("/");
764                        obsoleteURL.append(obsolete[i]);
765                }
766
767                splitDirURL = splitURL.toString();
768                obsoleteDirURL = obsoleteURL.toString();
769
770
771                //Files join themselves iteratively
772                splitDirPath = path;
773                for(int i=0;i<split.length;i++) {
774                        splitDirPath = new File(splitDirPath,split[i]);
775                }
776                obsoleteDirPath = path;
777                for(int i=0;i<obsolete.length;i++) {
778                        obsoleteDirPath = new File(obsoleteDirPath,obsolete[i]);
779                }
780        }
781
782        /**
783         * Converts a PDB ID into a filename with the proper extension
784         * @param pdbId
785         * @return The filename, e.g. "4hhb.pdb.gz"
786         */
787        protected abstract String getFilename(String pdbId);
788
789        /**
790         * Location of split files within the directory, as an array of paths.
791         * These will be joined with either slashes (for the URL) or the file
792         * separator (for directories). The returned results should be constant,
793         * to allow for caching.
794         * @return A list of directories, relative to the /pub/pdb directory on the server
795         */
796        protected abstract String[] getSplitDirPath();
797        /**
798         * Location of obsolete files within the directory, as an array of paths.
799         * These will be joined with either slashes (for the URL) or the file
800         * separator (for directories). The returned results should be constant,
801         * to allow for caching.
802         * @return A list of directories, relative to the /pub/pdb directory on the server
803         */
804        protected abstract String[] getObsoleteDirPath();
805}