001    package hirondelle.web4j.database;
002    
003    import java.util.regex.Pattern;
004    import hirondelle.web4j.model.Check;
005    import hirondelle.web4j.model.ModelCtorException;
006    import hirondelle.web4j.model.ModelUtil;
007    import hirondelle.web4j.util.EscapeChars;
008    import hirondelle.web4j.util.Util;
009    import hirondelle.web4j.util.Consts;
010    import hirondelle.web4j.util.Regex;
011    
012    /**
013    <span class="highlight">Identifier of an SQL statement block in an <tt>.sql</tt> file.</span>
014    (Such identifiers must be unique.)
015     
016     <P>This class does <em>not</em> contain the text of the underlying SQL statement.
017     Rather, this class allows a code friendly way of <em>referencing</em> SQL statements. 
018     Since <tt>.sql</tt> files are simple text files, there is a need to build a bridge between these text files
019     and java code. This class is that bridge. 
020     
021     <P> Please see the package summary for important information regarding <tt>.sql</tt> files.
022      
023     <P>Typical use case :
024    <PRE>public static final SqlId MEMBER_FETCH = new SqlId("MEMBER_FETCH");</PRE>
025    This corresponds to an entry in an <tt>.sql</tt> file :
026    <PRE>
027    MEMBER_FETCH {
028      SELECT Id, Name, IsActive, DispositionFK 
029      FROM Member WHERE Id=?
030    }
031    </PRE>
032    
033     <P>This class is unusual, since there is only one way to use these objects. 
034     That is, they <span class="highlight">must be declared  
035     as <tt>public static final</tt> fields in a <tt>public</tt> class.</span> 
036     They should never appear <i>only</i> as local objects in the body of a method. (This unusual restriction 
037     exists to allow the framework to find and examine such fields using reflection.)
038     The text passed to the constructor must correspond to the identifier of some SQL 
039     statement block in an <tt>.sql</tt> file. Such identifiers must match a specific 
040     {@link #FORMAT}.
041     
042     <P><a name="StartupChecks"></a><b>Startup Checks</b><br>
043     To discover simple typographical errors as quickly as possible, 
044     the framework will run diagnostics upon startup : <span class="highlight">there must be an exact, one-to-one 
045     correspondence between the SQL statement identifiers defined in the <tt>.sql</tt> file(s), 
046     and the <tt>public static final SqlId</tt> fields declared by the 
047     application.</span> Any mismatch will result in an error. (Running such diagnostics 
048     upon startup is highly advantageous, since the only alternative is discovery during 
049     actual use, upon the first execution of a particular operation.) 
050     
051     <P><a name="DeclarationLocation"></a><b>Where To Declare <tt>SqlId</tt> Fields</b><br>
052     Where should <tt>SqlId</tt> fields be declared? The only real restriction is that 
053     they must be declared in a <tt>public</tt> class. With the most recommended first, one may declare 
054     <tt>SqlId</tt> fields in :
055     <ul>
056     <li>a <tt>public</tt> {@link hirondelle.web4j.action.Action}
057     <li>a <tt>public</tt> Data Access Object
058     <li>a <tt>public</tt> constants class, one per package/feature. 
059     <li>a <tt>public</tt> constants class, one per application. If more than one developer at a time 
060     works on the application, then this style will result in a lot of developer contention. It is not recommended. 
061     </ul>
062     
063     <P><em>Design Note</em>
064     <br>The justification for recommending that <tt>SqlId</tt> fields appear in a 
065     {@link hirondelle.web4j.action.Action} is as follows : 
066     <ul>
067     <li>it is highly satisfying to have mostly <tt>package-private</tt> classes in an application, since it 
068     takes advantage of a principal technique for "information hiding" - one of the guiding principles of 
069     lasting value in object programming. For instance, it is usually possible to have a Data Access Object 
070     (DAO) as package-private. If a <tt>SqlId</tt> is declared in a DAO, however, then that DAO must be 
071     changed to <tt>public</tt>, just to render the <tt>SqlId</tt> fields accessible by reflection, 
072     which is distasteful.
073     <li>the {@link hirondelle.web4j.action.Action} is always <tt>public</tt> anyway, so adding a <tt>SqlId</tt> will 
074     not change its scope.
075     <li>{@link hirondelle.web4j.action.Action} is intended as the <tt>public</tt> face of each feature. Therefore, 
076     all important items related to the feature should be documented there - what it does, when is it called, and 
077     how it shows a response. One can argue with some force that the single most important thing about a 
078     feature is <em>"What does it do?"</em>. In a typical database application, the answer to that 
079     question is usually <em>"these SQL operations"</em>.  
080     </ul>
081    */
082    public final class SqlId {
083    
084      /**
085       Format of SQL statement identifiers.
086        
087       <P>Matching examples include : 
088      <ul>
089       <li><tt>ADD_MESSAGE</tt>
090       <li><tt>fetch_member</tt>
091       <li><tt>LIST_RESTAURANTS_2</tt>
092      </ul>
093      
094       <P>One or more letters/underscores, with possible trailing digits. 
095       <P>To scope an SQL statement to a particular database, simply prefix the identifier with a second 
096       such identifier to represent the database, separated by a period, 
097       as in <tt>'TRANSLATION_DB.ADD_BASE_TEXT'</tt>. 
098      */
099      public static final String FORMAT = Regex.SIMPLE_IDENTIFIER;
100       
101      /**
102       Constructor for statement against the <em>default</em> database. 
103        
104       @param aStatementName identifier of an SQL statement, satisfies  {@link #FORMAT}, 
105       and matches the name attached to an SQL statement appearing in an <tt>.sql</tt> file.
106      */
107      public SqlId(String aStatementName) { 
108        fStatementName = aStatementName;
109        fDatabaseName = null;
110        validateState();
111      }
112      
113      /**
114       Constructor for statement against a <em>named</em> database.
115       
116       @param aDatabaseName identifier for the target database, 
117       satisfies {@link #FORMAT}, 
118       matches one of the return values of {@link ConnectionSource#getDatabaseNames()},
119       and also matches the prefix for a <tt>aStatementName</tt>. See package overview for more information. 
120       @param aStatementName identifier of an SQL statement, satisfies  {@link #FORMAT}, 
121       and matches the name attached to an SQL statement appearing in an <tt>.sql</tt> file.
122      */
123      public SqlId(String aDatabaseName, String aStatementName) {
124        fStatementName = aStatementName;
125        fDatabaseName = aDatabaseName;
126        validateState();
127      }
128      
129      /**
130       Factory method for building an <tt>SqlId</tt> from a <tt>String</tt> which may or may 
131       not be qualified by the database name. 
132       
133       @param aSqlId which may or may not be qualified by the database name.
134      */
135      public static SqlId fromStringId(String aSqlId){
136        SqlId result = null;
137        String SEPARATOR = ".";
138        if( aSqlId.contains(SEPARATOR) ){
139          String[] parts = aSqlId.split(EscapeChars.forRegex(SEPARATOR));
140          String database = parts[0];
141          String statement = parts[1];
142          result = new SqlId(database, statement);
143        }
144        else {
145          result = new SqlId(aSqlId);
146        }
147        return result;
148      }
149      
150      /**
151       Return <tt>aDatabaseName</tt> passed to the constructor. 
152       
153       <P>If no database name was passed to the constructor, then return an empty {@link String} 
154       (corresponds to the 'default' database).
155       
156      */
157      public String getDatabaseName(){
158        return Util.textHasContent(fDatabaseName) ? fDatabaseName : Consts.EMPTY_STRING;
159      }
160      
161      /** Return <tt>aStatementName</tt> passed to the constructor.  */
162      public String getStatementName(){
163        return fStatementName;
164      }
165      
166      /**
167       Return the SQL statement identifier as it appears in the <tt>.sql</tt> file.
168       
169       <P>Example return values :
170      <ul>
171       <li><tt>MEMBER_FETCH</tt> (against the default database)
172       <li><tt>TRANSLATION.FETCH_ALL_TRANSLATIONS</tt> (against a database named <tt>TRANSLATION</tt>)
173      </ul> 
174      */
175      @Override public String toString() { 
176        return Util.textHasContent(fDatabaseName) ? fDatabaseName + "." + fStatementName : fStatementName;  
177      } 
178      
179      @Override public boolean equals(Object aThat){
180        Boolean result = ModelUtil.quickEquals(this, aThat);
181        if( result == null ) {
182          SqlId that = (SqlId) aThat;
183          result = ModelUtil.equalsFor(this.getSignificantFields(), that.getSignificantFields());
184        }
185        return result;
186      }
187     
188      @Override public int hashCode(){
189        if(fHashCode == 0){
190          fHashCode =  ModelUtil.hashCodeFor(getSignificantFields()); 
191        }
192        return fHashCode;
193      }
194       
195      // PRIVATE //
196      private final String fStatementName;
197      private final String fDatabaseName;
198      private int fHashCode;
199    
200      /**
201       Does NOT throw ModelCtorException, since errors here represent bugs. 
202      */
203      private void validateState(){
204        ModelCtorException ex = new ModelCtorException();
205        Pattern simpleId = Pattern.compile(FORMAT);
206        if ( ! Check.required(fStatementName, Check.pattern(simpleId)) ) {
207          ex.add("Statement Name is required, and must match SqlId.FORMAT.");
208        }
209        if ( ! Check.optional(fDatabaseName, Check.pattern(simpleId)) ) {
210          ex.add("Database Name is optional, and must match SqlId.FORMAT.");
211        }
212        if ( ! ex.isEmpty() ) {
213          throw new IllegalArgumentException(Util.logOnePerLine(ex.getMessages()));
214        }
215      }
216      
217      private Object[] getSignificantFields(){
218        return new Object[]{fStatementName, fDatabaseName};
219      }
220    }