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