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 */
021package org.biojava.nbio.structure;
022
023import java.io.Serializable;
024import java.util.regex.Pattern;
025
026/**
027 * A wrapper class for the PDB identifier.
028 * 
029 * It handles conversion between current (short) <code>[1-9][0-9A-Z]{3}</code> and
030 * upcoming (extended) <code>PDB_\d{4}[1-9][09-A-Z]</code> PDB ID format.<br>
031 * Instances of this class are <em>immutable</em>.<br>
032 * Creation of PdBId instance follows strict PDB ID convention.
033 * There is only one exception to this rule which is <b>XXXX</b>. XXXX objects 
034 * are not considered equal (unless they are the one and the same object).
035 * @author Amr ALHOSSARY
036 * @since 6.0.0
037 *
038 */
039public class PdbId implements Comparable<PdbId>, Serializable{
040        
041        private static final long serialVersionUID = -7740865530486255113L;
042        private static final String PREFIX_PDB_ = "PDB_";
043        private static final String STRING_0000 = "0000";
044        private static final String PDB_0000 = PREFIX_PDB_ + STRING_0000;
045
046        /**
047         * Controls how the PDB ID output/conversion should go, if possible.
048         * The default is to try to produce short PDB ID. If failed, produce extended PDB ID.
049         */
050        private static final boolean defaultShorteningBehaviour = true;
051
052
053        /**
054         * A regular expression that matches a PDB ID in the short format.
055         */
056        public static final Pattern PATTERN_SHORT_PDBID = Pattern.compile("[1-9]\\p{Alnum}{3}");
057        /**
058         * A regular expression that matches a PDB ID in the extended format.
059         */
060        public static final Pattern PATTERN_EXTENDED_PDBID = Pattern.compile("(pdb|PDB)_\\p{Alnum}{8}"); 
061        /**
062/        * A regular expression that matches an extended PDB ID that is compatible with the short format.
063         */
064        public static final Pattern PATTERN_SHORTABLE_EXTENDED_PDBID = Pattern.compile("(pdb|PDB)_0000[1-9]\\p{Alnum}{3}"); 
065
066        /**
067         * Keeps the ID in <b>UPPER CASE</b>, in a <em>reduced</em> form (without the <code>PDB_</code> prefix).
068         */
069        private String idCode;
070
071        /**
072         * @param id A <i>valid</i> PDB ID in either <i>short (case insensitive)</i> or <i>extended</i> format.
073         * @throws IllegalArgumentException If <code>id</code> is not a valid identifier.
074         * @throws NullPointerException  If <code>id</code> is <code>null</code>.
075         */
076        public PdbId(String id){
077                if (id == null) {
078                        throw new IllegalArgumentException("ID can not be null");
079                }
080                this.idCode = toInternalFormat(id);
081        }
082        
083        /**
084         * Check whether <code>id</code> represents a valid PDB ID in the <em>short</em> format.
085         * @param id Prospect ID
086         * @return <code>true</code> if <code>id</code> is a valid short PDB ID, <code>false</code> otherwise.
087         * @throws NullPointerException if <code>id</code> is <code>null</code>.
088         * @see #isValidExtendedPdbId(String)
089         */
090        public static boolean isValidShortPdbId(String id) {
091                return PATTERN_SHORT_PDBID.matcher(id).matches();
092        }
093        
094        /**
095         * Check whether <code>id</code> represents a valid PDB ID in the <em>extended</em> format.
096         * @param id Prospect ID
097         * @return <code>true</code> if <code>id</code> is a valid extended PDB ID, <code>false</code> otherwise.
098         * @throws NullPointerException if <code>id</code> is <code>null</code>.
099         * @see #isValidShortPdbId(String)
100         */
101        public static boolean isValidExtendedPdbId(String id) {
102                return PATTERN_EXTENDED_PDBID.matcher(id).matches();
103        }
104        
105        /**
106         * Checks whether an Extended PDB ID is shortable, <i>assuming it is a valid extended PDB ID</i>.
107         * @see #isValidExtendedPdbId(String)
108         * @param extendedId the supposedly valid extended PDB ID.
109         * @return <code>true</code> if <code>extendedId</code> can be shortened 
110         * (ie. it matches the regular expression "(pdb|PDB)_0000[1-9][a-zA-Z0-9]{3}"), <code>false</code> otherwise.
111         */
112        public static boolean isShortCompatible(String extendedId) {
113                return PATTERN_SHORTABLE_EXTENDED_PDBID.matcher(extendedId).matches();
114        }
115        
116        @Override
117        public int hashCode() {
118                return idCode.hashCode();
119        }
120        
121        @Override
122        public boolean equals(Object obj) {
123                if (this == obj)
124                        return true;
125                if (obj == null)
126                        return false;
127                if (getClass() != obj.getClass())
128                        return false;
129                // We are sure they are both objects of the same class and their respective IDs are in the same (UPPER) case.
130                return this.idCode.equals(((PdbId)obj).idCode);
131        }
132        
133        @Override
134        protected Object clone() throws CloneNotSupportedException {
135                return new PdbId(this.getId());
136        }
137
138        @Override
139        public String toString() {
140                return getId();
141        }
142
143        /**
144         * Get a <code>String</code> representation of this PdbId instance.<br>
145         * By default this function will try to get the PdbId in the short (4 letters) format.
146         * If not possible, it will return the long format.
147         * N.B. This default behavior may change later;
148         * @return the PdbId code, preferably in short format.
149         */
150        public String getId() {
151                return getId(defaultShorteningBehaviour);
152        }
153        
154        /**
155         * Get a <code>String</code> representation of this PdbId instance, using the <i>passed in</i> behavior.<br>
156         * @param prefereShort when it is <code>true</code>, the class will try to produce the short ID whenever possible.
157         * @return The PdbId in short format if possible and <code>prefereShort</code> is <code>true</code>, the extended PDB ID form otherwise.
158         */
159        public String getId(boolean prefereShort) {
160                if (prefereShort && isInternalShortCompatible(idCode))
161                        return internalToShortNoCheck(idCode);
162                return PREFIX_PDB_ + idCode;
163        }
164        
165        /**
166         * Get the PDB Id in the short format. Throws an exception if the conversion is not possible.<br>
167         * Use this method only if you know that this PDB ID is shortable.
168         * @return the PDB ID in the short format.
169         * @throws StructureException if the conversion was not possible.
170         */
171        public String getShortId() throws StructureException{
172                if(isInternalShortCompatible(idCode)) {
173                        return internalToShortNoCheck(idCode);
174                } else {
175                        throw new StructureException("ID (" + getId() + ") is not short format compatible");
176                }
177        }
178        
179        /**
180         * Converts <code>shortId</code> to the PDB ID extended format.
181         * If <code>shortId</code> is a valid short PDB ID, it would be converted to an extended ID,
182         * if <code>shortId</code> is a valid extended PDB ID, it would be returned in UPPER CASE,
183         * a {@link StructureException} is thrown otherwise.
184         * @param shortId the PDB ID to convert to extended format
185         * @return the ID in the extended UPPER CASE format.
186         * @throws StructureException if the conversion was not possible.
187         */
188        public static String toExtendedId(String shortId) throws StructureException{
189                if (isValidShortPdbId(shortId)) {
190                        return PDB_0000 + shortId.toUpperCase();
191                }else if (isValidExtendedPdbId(shortId)) {
192                        return shortId.toUpperCase();
193                } else {
194                        throw new StructureException("Unknown format ["+shortId+"]");
195                }
196        }
197        
198        /**
199         * Converts <code>extendedId</code> to the PDB ID short format.
200         * If <code>extendedId</code> is a valid extended PDB ID, it would be converted to a short ID,
201         * if <code>extendedId</code> is a valid short PDB ID, it would be returned in UPPER CASE,
202         * a {@link StructureException} is thrown otherwise.
203         * @param extendedId the PDB ID to convert to short format
204         * @return the ID in the short UPPER CASE format.
205         * @throws StructureException if the conversion was not possible.
206         */
207        public static String toShortId(String extendedId) throws StructureException{
208                if (isShortCompatible(extendedId)) {
209                        return extendedId.substring(8).toUpperCase();
210                } else if (isValidShortPdbId(extendedId)) {
211                        return extendedId.toUpperCase();
212                } else {
213                        throw new StructureException("Conversion not possible of ID ["+extendedId+"]");
214                }
215        }
216
217        private static boolean isInternalShortCompatible(String intId) {
218                return intId.substring(0, 4).equals(STRING_0000);
219        }
220        
221        private static String toInternalFormat(String id) throws IllegalArgumentException {
222                if (isValidShortPdbId(id)) {
223                        return STRING_0000  + id.toUpperCase();
224                }else if (isValidExtendedPdbId(id)) {
225                        return id.substring(4).toUpperCase();
226                } else {
227                        throw new IllegalArgumentException("Unknown format [" + id + "]");
228                }
229        }
230        
231        private static String internalToShortNoCheck(String extendedId) {
232                return extendedId.substring(4).toUpperCase();
233        }
234        
235        @Override
236        public int compareTo(PdbId o) {
237                //We know that both idCode fields are 8 UPPER CASE characters strings.
238                return this.idCode.compareTo(o.idCode);
239        }
240        
241}