package hirondelle.web4j.database;
import java.util.*;
import hirondelle.web4j.request.Formats;
import hirondelle.web4j.security.SafeText;
import hirondelle.web4j.util.Util;
/**
Utility for producing reports quickly from a <tt>ResultSet</tt>.
<P>Most web applications need to produce reports of summary or transaction information.
If a Model Object already exists for the given data, then it may be used to fetch and render the data, likely
with {@link Db}.
<P>If a Model Object does <em>not</em> already exist for the given report data, then this class may
be used to quickly implement the report, without the need to create a full Model Object.
<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
robust Model Object, but rather as a rather dumb data carrier, built only for the purpose of reporting.
The <tt>Map</tt> key is always the column name, and the <tt>Map</tt> value takes various forms, according
to how the <tt>ResultSet</tt> is processed :
<ul>
<li>{@link #formatted(Class[], Locale, TimeZone, SqlId, DynamicCriteria, Object[])} - column values are parsed using
{@link ConvertColumn} into <tt>Integer</tt>, <tt>Date</tt>, and so on, and then standard formatting is applied
using {@link Formats#objectToText(Object)}. (This is the recommended style.)
<li>{@link #raw(SqlId, DynamicCriteria, Object[] )} - column values are all treated as simple text. To allow for various escaping styles in the view,
the text is actually returned as {@link SafeText}. By default, the text will be properly escaped for presentation
in HTML. If desired, some formatting can be applied directly in the underlying SQL statement itself, using database
formatting functions.
<li>{@link #unformatted(Class[], SqlId, DynamicCriteria, Object[])} - column values are parsed using {@link ConvertColumn} into
<tt>Integer</tt>, <tt>Date</tt>, and so on, but formatting is deferred to the JSP.
</ul>
<P>Example of using the <tt>Map</tt> in a JSP. Here, column values are assumed to be already formatted, using
either {@link #raw(SqlId, DynamicCriteria, Object[])} or {@link #formatted(Class[], Locale, TimeZone, SqlId, DynamicCriteria, Object[])}:
<PRE>
{@code
<c:forEach var="row" items="${reportMap}" >
<tr>
<td>${row['Name']}</td>
<td>${row['Visits']}</td>
</tr>
</c:forEach>
}
</PRE>
<P>If {@link #unformatted(Class[], SqlId, DynamicCriteria, Object[])} is used to build the <tt>Map</tt>, then formatting
of the resulting objects must be applied in the JSP.
<h3>Recommended Style</h3>
The recommended style is to use {@link #formatted(Class[], Locale, TimeZone, SqlId, DynamicCriteria, Object[])}.
<P>If {@link #raw(SqlId, DynamicCriteria, Object[])} or {@link #unformatted(Class[], SqlId, DynamicCriteria, Object[])} is used, then
the question usually arises of where to apply formatting :
<ul>
<li>format with database formatting functions - then there will often be much repetition of formatting function calls
across different <tt>SELECT</tt>s.
<li>format in the JSP (usually with JSTL) - be aware that there is often
significant work involved. Not one but three operations can be necessary : a parse operation,
a format operation, and possibly a check-for-null.
</ul>
<h3><a name="emptyContent"></a>Empty Values</h3>
When the <tt>Map</tt> returned by this class has values as text, then any <tt>Strings</tt> which do not satisfy
{@link Util#textHasContent(String)} are replaced with the
return value of {@link hirondelle.web4j.request.Formats#getEmptyOrNullText} (which is in
turn configured in <tt>web.xml</tt>). This is a workaround for the fact that most browsers do
not render empty <tt>TD</tt> tags very well when the cell has a border. An alternate
(and likely superior) workaround is to set the
<tt><a href="http://www.w3.org/TR/REC-CSS2/tables.html#empty-cells">empty-cells</a></tt>
property of Cascading Style Sheets to <tt>'show'</tt>.
*/
public final class Report {
/**
Return column values without any processing.
<P>For the returned {@code Map<String, SafeText>} objects,
<ul>
<li>key is the column name
<li>value is the unprocessed column value, passed to a {@link SafeText}. <tt>SafeText</tt> is
used instead of <tt>String</tt> to allow easy escaping of special characters in the view.
</ul>
@param aSqlId identifies the underlying <tt>SELECT</tt> statement
@param aParams parameters for the <tt>SELECT</tt> statement, in the same order as in the underlying <tt>SELECT</tt> statement
*/
public static List<Map<String, SafeText>> raw(SqlId aSqlId, DynamicCriteria aCriteria, Object... aParams) throws DAOException {
List<Map<String,SafeText>> result = new ArrayList<Map<String, SafeText>>();
ModelBuilder<Map<String, SafeText>> builder = new ReportBuilder();
SqlFetcher fetcher = getFetcher(aSqlId, aCriteria, aParams);
fetcher.fetchObjects(builder, result);
return result;
}
/**
Return column values after processing into formatted building block objects.
<P>For the returned {@code Map<String, SafeText>} objects,
<ul>
<li>key is the column name
<li>value is the processed column value, as <tt>SafeText</tt>. Column values are first parsed into
building block objects using {@link ConvertColumn}. Then the objects are formatted in a 'standard'
way using the configured {@link Formats}.
</ul>
@param aTargetClasses defines the target class for each column.
The order of the classes in the array corresponds one-to-one with the column order of the underlying <tt>ResultSet</tt>.
The size of the array matches the number of columns.
Each class in the array must be supported by the configured {@link ConvertColumn}.
@param aLocale <tt>Locale</tt> returned by {@link hirondelle.web4j.request.LocaleSource}
@param aTimeZone <tt>TimeZone</tt> returned by {@link hirondelle.web4j.request.TimeZoneSource}
@param aSqlId identifies the underlying <tt>SELECT</tt> statement
@param aCriteria possible <em>dynamic</em> <tt>WHERE</tt> or <tt>ORDER BY</tt> clause. If no dynamic criteria, then
just pass {@link DynamicCriteria#NONE}.
@param aParams parameters for the <tt>SELECT</tt> statement, in the same order as in the underlying <tt>SELECT</tt> statement
*/
public static List<Map<String, SafeText>> formatted(Class<?>[] aTargetClasses, Locale aLocale, TimeZone aTimeZone, SqlId aSqlId, DynamicCriteria aCriteria, Object... aParams) throws DAOException {
List<Map<String, SafeText>> result = new ArrayList<Map<String, SafeText>>();
//any date columns must be formatted in a Locale-sensitive manner
Formats formats = new Formats(aLocale, aTimeZone);
ModelBuilder<Map<String, SafeText>> builder = new ReportBuilder(aTargetClasses, formats);
SqlFetcher fetcher = getFetcher(aSqlId, aCriteria, aParams);
fetcher.fetchObjects(builder, result);
return result;
}
/**
Return column values as unformatted building block objects.
<P>For the returned {@code Map<String, Object>} objects,
<ul>
<li>key is the column name
<li>value is the processed column value, parsed into a building block Object using {@link ConvertColumn}.
</ul>
@param aTargetClasses defines the target class for each column.
The order of the classes in the array corresponds one-to-one with the column order of the underlying <tt>ResultSet</tt>.
The size of the array matches the number of columns.
Each class in the array must be supported by the configured {@link ConvertColumn}.
@param aSqlId identifies the underlying <tt>SELECT</tt> statement
@param aCriteria possible <em>dynamic</em> <tt>WHERE</tt> or <tt>ORDER BY</tt> clause. If no dynamic criteria, then
just pass {@link DynamicCriteria#NONE}.
@param aParams parameters for the <tt>SELECT</tt> statement, in the same order as in the underlying <tt>SELECT</tt> statement
*/
public static List<Map<String, Object>> unformatted(Class<?>[] aTargetClasses, SqlId aSqlId, DynamicCriteria aCriteria, Object... aParams) throws DAOException {
List<Map<String, Object>> result = new ArrayList<Map<String, Object>>();
ModelBuilder<Map<String, Object>> builder = new ReportBuilderUnformatted(aTargetClasses);
SqlFetcher fetcher = getFetcher(aSqlId, aCriteria, aParams);
fetcher.fetchObjects(builder, result);
return result;
}
// PRIVATE //
private Report(){
//empty - prevent construction by caller
}
private static SqlFetcher getFetcher(SqlId aSqlId, DynamicCriteria aCriteria, Object[] aParams) throws DAOException {
SqlFetcher result = null;
if( DynamicCriteria.NONE == aCriteria ) {
result = SqlFetcher.forSingleOp(aSqlId, aParams);
}
else {
result = SqlFetcher.forSearch(aSqlId, aCriteria, aParams);
}
return result;
}
}