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