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 }