001    package hirondelle.web4j.model;
002    
003    import java.io.Serializable;
004    import java.io.ObjectInputStream;
005    import java.io.ObjectOutputStream;
006    import java.io.IOException;
007    import hirondelle.web4j.model.ModelUtil;
008    import hirondelle.web4j.util.Consts;
009    import hirondelle.web4j.util.Util;
010    import hirondelle.web4j.security.SafeText;
011    
012    /**
013     Building block class for identifiers. 
014     
015     <P>Identifiers are both common and important. Unfortunately, there is no class in the 
016     JDK specifically for identifiers. 
017     
018     <P>An <tt>Id</tt> class is useful for these reasons :
019    <ul>
020     <li>it allows model classes to read at a higher level of abstraction. Identifiers are 
021     labeled as such, and stand out very clearly from other items
022     <li>it avoids a common <a href="http://www.javapractices.com/Topic192.cjp">problem</a> 
023     in modeling identifiers as numbers.
024    </ul>
025    
026     <P>The underlying database column may be modeled as either text or as a number.
027     If the underlying column is of a numeric type, however, then a Data Access Object
028     will need to pass <tt>Id</tt> parameters to {@link hirondelle.web4j.database.Db} 
029     using {@link #asInteger} or {@link #asLong}.
030    
031     <P><em>Design Note :</em><br>
032     This class is <tt>final</tt>, immutable, {@link Serializable}, 
033     and {@link Comparable}, in imitation of the other building block classes 
034     such as {@link String}, {@link Integer}, and so on.
035    */
036    public final class Id implements Serializable, Comparable<Id> {
037    
038      /**
039       Construct an identifier using an arbitrary {@link String}.
040        
041       This class uses a {@link SafeText} object internally.
042       @param aText is non-null, and contains characters that are allowed 
043       by {@link hirondelle.web4j.security.PermittedCharacters}.
044      */
045      public Id(String aText) {
046        fId = new SafeText(aText);
047        validateState();
048      }
049      
050      /**
051       Factory method.
052       
053       Simply a slightly more compact way of building an object, as opposed to 'new'.
054      */
055      public static Id from(String aText){
056        return new Id(aText);
057      }
058    
059      /**
060       Return this id as an {@link Integer}, if possible.
061       
062       <P>See class comment.
063       
064       <P>If this <tt>Id</tt> is not convertible to an {@link Integer}, then a {@link RuntimeException} is 
065       thrown.
066      */
067      public Integer asInteger(){
068        return new Integer(fId.getRawString());
069      }
070    
071      /**
072       Return this id as a {@link Long}, if possible.
073       
074       <P>See class comment.
075       
076       <P>If this <tt>Id</tt> is not convertible to a {@link Long}, 
077       then a {@link RuntimeException} is thrown.
078      */
079      public Long asLong(){
080        return new Long(fId.getRawString());
081      }
082      
083      /**
084       Return the id, with special characters escaped. 
085       
086       <P>The return value either has content (with no leading or trailing spaces), 
087       or is empty.
088       See {@link hirondelle.web4j.util.EscapeChars#forHTML(String)} for a list of escaped
089       characters.
090      */
091      @Override public String toString(){
092        return fId.toString();
093      }
094      
095      /** Return the text passed to the constructor. */
096      public String getRawString(){
097        return fId.getRawString();
098      }
099      
100      /** Return the text with special  XML characters esacped. See {@link SafeText#getXmlSafe()}.  */
101      public String getXmlSafe() {
102        return fId.getXmlSafe();
103      }
104      
105      @Override public boolean equals(Object aThat){
106        Boolean result = ModelUtil.quickEquals(this, aThat);
107        if ( result == null ){
108          Id that = (Id) aThat;
109          result = ModelUtil.equalsFor(this.getSignificantFields(), that.getSignificantFields());
110        }
111        return result;    
112      }
113    
114      @Override public int hashCode(){
115        return ModelUtil.hashCodeFor(getSignificantFields());
116      }
117      
118      public int compareTo(Id aThat) {
119        final int EQUAL = 0;
120        if ( this == aThat ) return EQUAL;
121        int comparison = this.fId.compareTo(aThat.fId);
122        if ( comparison != EQUAL ) return comparison;
123        return EQUAL;
124      }
125      
126      // PRIVATE 
127      
128      /** @serial  */
129      private SafeText fId;
130      
131      /**
132       For evolution of this class, see Sun guidelines : 
133       http://java.sun.com/j2se/1.5.0/docs/guide/serialization/spec/version.html#6678 
134      */
135      private static final long serialVersionUID = 7526472295633676147L;
136      
137      /**
138       Always treat de-serialization as a full-blown constructor, by
139       validating the final state of the de-serialized object.
140      */
141      private void readObject(ObjectInputStream aInputStream) throws ClassNotFoundException, IOException {
142         //always perform the default de-serialization first
143         aInputStream.defaultReadObject();
144         //make defensive copy of mutable fields (none here)
145         //ensure that object state has not been corrupted or tampered with maliciously
146         validateState();
147      }
148    
149      /**
150       This is the default implementation of writeObject.
151       Customise if necessary.
152      */
153      private void writeObject(ObjectOutputStream aOutputStream) throws IOException {
154        //perform the default serialization for all non-transient, non-static fields
155        aOutputStream.defaultWriteObject();
156      }
157      
158      private void validateState() {
159        if( ! Util.textHasContent(fId) ) {
160          if ( ! Consts.EMPTY_STRING.equals(fId.getRawString()) ) {
161            throw new IllegalArgumentException(
162              "Id must have content, or be the empty String. Erroneous Value : " + Util.quote(fId)
163            );
164          }
165        }
166      }
167      
168      private Object[] getSignificantFields(){
169        return new Object[] {fId};
170      }
171    }