001 package hirondelle.web4j.database; 002 003 import java.util.*; 004 005 import hirondelle.web4j.request.Formats; 006 import hirondelle.web4j.security.SafeText; 007 import hirondelle.web4j.util.Util; 008 009 /** 010 Utility for producing reports quickly from a <tt>ResultSet</tt>. 011 012 <P>Most web applications need to produce reports of summary or transaction information. 013 If a Model Object already exists for the given data, then it may be used to fetch and render the data, likely 014 with {@link Db}. 015 016 <P>If a Model Object does <em>not</em> already exist for the given report data, then this class may 017 be used to quickly implement the report, without the need to create a full Model Object. 018 019 <P>This class translates each <tt>ResultSet</tt> row into a <tt>Map</tt> of some form. This <tt>Map</tt> is meant not as a 020 robust Model Object, but rather as a rather dumb data carrier, built only for the purpose of reporting. 021 The <tt>Map</tt> key is always the column name, and the <tt>Map</tt> value takes various forms, according 022 to how the <tt>ResultSet</tt> is processed : 023 <ul> 024 <li><tt>formatted</tt> - column values are parsed using 025 {@link ConvertColumn} into <tt>Integer</tt>, <tt>Date</tt>, and so on, and then standard formatting is applied 026 using {@link Formats#objectToText(Object)}. (This is the recommended style.) 027 <li><tt>unformatted</tt> - column values are parsed using {@link ConvertColumn} into 028 <tt>Integer</tt>, <tt>Date</tt>, and so on, but formatting is deferred to the JSP. 029 <li><tt>raw</tt> - column values are all treated as simple text. To allow for various escaping styles in the view, 030 the text is actually returned as {@link SafeText}. By default, the text will be properly escaped for presentation 031 in HTML. If desired, some formatting can be applied directly in the underlying SQL statement itself, using database 032 formatting functions. 033 </ul> 034 035 <P>Example of using the <tt>Map</tt> in a JSP. Here, column values are assumed to be already formatted, using 036 either <tt>raw</tt> or <tt>formatted</tt>: 037 <PRE> 038 {@code 039 <c:forEach var="row" items="${reportMap}" > 040 <tr> 041 <td>${row['Name']}</td> 042 <td>${row['Visits']}</td> 043 </tr> 044 </c:forEach> 045 } 046 </PRE> 047 048 <P>If <tt>unformatted</tt> is used to build the <tt>Map</tt>, then formatting 049 of the resulting objects must be applied in the JSP. 050 051 <h3>Recommended Style</h3> 052 The recommended style is to use <tt>formatted</tt>. 053 054 <P>If <tt>raw</tt> or <tt>unformatted</tt> is used, then 055 the question usually arises of where to apply formatting: 056 <ul> 057 <li>format with database formatting functions - then there will often be much repetition of formatting function calls 058 across different <tt>SELECT</tt>s. 059 <li>format in the JSP (usually with JSTL) - be aware that there is often 060 significant work involved. Not one but three operations can be necessary: a parse operation, 061 a format operation, and possibly a check-for-null. 062 </ul> 063 064 <h3><a name="emptyContent"></a>Empty Values</h3> 065 When the <tt>Map</tt> returned by this class has values as text, then any <tt>Strings</tt> which do not satisfy 066 {@link Util#textHasContent(String)} are replaced with the 067 return value of {@link hirondelle.web4j.request.Formats#getEmptyOrNullText} (which is in 068 turn configured in <tt>web.xml</tt>). This is a workaround for the fact that most browsers do 069 not render empty <tt>TD</tt> tags very well when the cell has a border. An alternate 070 (and likely superior) workaround is to set the 071 <tt><a href="http://www.w3.org/TR/REC-CSS2/tables.html#empty-cells">empty-cells</a></tt> 072 property of Cascading Style Sheets to <tt>'show'</tt>. 073 */ 074 public final class Report { 075 076 /** 077 Return column values without any processing. 078 079 <P>For the returned {@code Map<String, SafeText>} objects, 080 <ul> 081 <li>key is the column name 082 <li>value is the unprocessed column value, passed to a {@link SafeText}. <tt>SafeText</tt> is 083 used instead of <tt>String</tt> to allow easy escaping of special characters in the view. 084 </ul> 085 086 @param aSqlId identifies the underlying <tt>SELECT</tt> statement 087 @param aCriteria possible <em>dynamic</em> <tt>WHERE</tt> or <tt>ORDER BY</tt> clause. If no dynamic criteria, then 088 just pass {@link DynamicSql#NONE}. 089 @param aParams parameters for the <tt>SELECT</tt> statement, in the same order as in the underlying <tt>SELECT</tt> statement 090 */ 091 public static List<Map<String, SafeText>> raw(SqlId aSqlId, DynamicSql aCriteria, Object... aParams) throws DAOException { 092 List<Map<String,SafeText>> result = new ArrayList<Map<String, SafeText>>(); 093 ModelBuilder<Map<String, SafeText>> builder = new ReportBuilder(); 094 SqlFetcher fetcher = getFetcher(aSqlId, aCriteria, aParams); 095 fetcher.fetchObjects(builder, result); 096 return result; 097 } 098 099 /** 100 Return column values after processing into formatted building block objects. 101 102 <P>For the returned {@code Map<String, SafeText>} objects, 103 <ul> 104 <li>key is the column name 105 <li>value is the processed column value, as <tt>SafeText</tt>. Column values are first parsed into 106 building block objects using {@link ConvertColumn}. Then the objects are formatted in a 'standard' 107 way using the configured {@link Formats}. 108 </ul> 109 110 @param aTargetClasses defines the target class for each column. 111 The order of the classes in the array corresponds one-to-one with the column order of the underlying <tt>ResultSet</tt>. 112 The size of the array matches the number of columns. 113 Each class in the array must be supported by the configured {@link ConvertColumn}. 114 @param aLocale <tt>Locale</tt> returned by {@link hirondelle.web4j.request.LocaleSource} 115 @param aTimeZone <tt>TimeZone</tt> returned by {@link hirondelle.web4j.request.TimeZoneSource} 116 @param aSqlId identifies the underlying <tt>SELECT</tt> statement 117 @param aCriteria possible <em>dynamic</em> <tt>WHERE</tt> or <tt>ORDER BY</tt> clause. If no dynamic criteria, then 118 just pass {@link DynamicSql#NONE}. 119 @param aParams parameters for the <tt>SELECT</tt> statement, in the same order as in the underlying <tt>SELECT</tt> statement 120 */ 121 public static List<Map<String, SafeText>> formatted(Class<?>[] aTargetClasses, Locale aLocale, TimeZone aTimeZone, SqlId aSqlId, DynamicSql aCriteria, Object... aParams) throws DAOException { 122 List<Map<String, SafeText>> result = new ArrayList<Map<String, SafeText>>(); 123 //any date columns must be formatted in a Locale-sensitive manner 124 Formats formats = new Formats(aLocale, aTimeZone); 125 ModelBuilder<Map<String, SafeText>> builder = new ReportBuilder(aTargetClasses, formats); 126 SqlFetcher fetcher = getFetcher(aSqlId, aCriteria, aParams); 127 fetcher.fetchObjects(builder, result); 128 return result; 129 } 130 131 /** 132 Return column values as unformatted building block objects. 133 134 <P>For the returned {@code Map<String, Object>} objects, 135 <ul> 136 <li>key is the column name 137 <li>value is the processed column value, parsed into a building block Object using {@link ConvertColumn}. 138 </ul> 139 140 @param aTargetClasses defines the target class for each column. 141 The order of the classes in the array corresponds one-to-one with the column order of the underlying <tt>ResultSet</tt>. 142 The size of the array matches the number of columns. 143 Each class in the array must be supported by the configured {@link ConvertColumn}. 144 @param aSqlId identifies the underlying <tt>SELECT</tt> statement 145 @param aCriteria possible <em>dynamic</em> <tt>WHERE</tt> or <tt>ORDER BY</tt> clause. If no dynamic criteria, then 146 just pass {@link DynamicSql#NONE}. 147 @param aParams parameters for the <tt>SELECT</tt> statement, in the same order as in the underlying <tt>SELECT</tt> statement 148 */ 149 public static List<Map<String, Object>> unformatted(Class<?>[] aTargetClasses, SqlId aSqlId, DynamicSql aCriteria, Object... aParams) throws DAOException { 150 List<Map<String, Object>> result = new ArrayList<Map<String, Object>>(); 151 ModelBuilder<Map<String, Object>> builder = new ReportBuilderUnformatted(aTargetClasses); 152 SqlFetcher fetcher = getFetcher(aSqlId, aCriteria, aParams); 153 fetcher.fetchObjects(builder, result); 154 return result; 155 } 156 157 // PRIVATE // 158 159 private Report(){ 160 //empty - prevent construction by caller 161 } 162 163 private static SqlFetcher getFetcher(SqlId aSqlId, DynamicSql aCriteria, Object[] aParams) throws DAOException { 164 SqlFetcher result = null; 165 if( DynamicSql.NONE == aCriteria ) { 166 result = SqlFetcher.forSingleOp(aSqlId, aParams); 167 } 168 else { 169 result = SqlFetcher.forSearch(aSqlId, aCriteria, aParams); 170 } 171 return result; 172 } 173 }