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 be used 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 }