001    package hirondelle.web4j.database;
002    
003    import hirondelle.web4j.BuildImpl;
004    import hirondelle.web4j.model.ConvertParam;
005    import hirondelle.web4j.model.DateTime;
006    import hirondelle.web4j.model.Decimal;
007    import hirondelle.web4j.model.Id;
008    import hirondelle.web4j.readconfig.Config;
009    import hirondelle.web4j.security.SafeText;
010    import hirondelle.web4j.util.Util;
011    
012    import java.io.InputStream;
013    import java.math.BigDecimal;
014    import java.sql.Clob;
015    import java.sql.ResultSet;
016    import java.sql.ResultSetMetaData;
017    import java.sql.SQLException;
018    import java.sql.Types;
019    import java.util.Locale;
020    import java.util.TimeZone;
021    import java.util.logging.Level;
022    import java.util.logging.Logger;
023    
024    /**
025     Default implementation of {@link ConvertColumn}, suitable for most applications.
026     
027     <P>This class converts non-<tt>null</tt> items using :
028     <table border='1' cellspacing='0' cellpadding='3'>
029      <tr><th>Target Class</th><th>Use</th></tr>
030      <tr><td><tt> SafeText </tt></td><td><tt> ResultSet.getString(), or ResultSet.getClob() </tt></td></tr>
031      <tr><td><tt> String (if allowed) </tt></td><td><tt> ResultSet.getString(), or ResultSet.getClob() </tt></td></tr>
032      <tr><td><tt> Integer </tt></td><td><tt>ResultSet.getInt()</tt></td></tr>
033      <tr><td><tt> Long </tt></td><td><tt> ResultSet.getLong() </tt></td></tr>
034      <tr><td><tt> Boolean </tt></td><td><tt> ResultSet.getBoolean() </tt></td></tr>
035      <tr><td><tt> BigDecimal </tt></td><td><tt> ResultSet.getBigDecimal() </tt></td></tr>
036      <tr><td><tt> Decimal</tt></td><td><tt> ResultSet.getBigDecimal()</tt></td></tr>
037      <tr><td><tt> Id </tt></td><td><tt> ResultSet.getString(), new Id(String)</tt></td></tr>
038      <tr><td><tt> DateTime </tt></td><td><tt> ResultSet.getString()</tt>, pass to {@link DateTime#DateTime(String)}</td></tr>
039      <tr><td><tt> Date </tt></td><td><tt> ResultSet.getTimestamp()</tt>, possibly with hint provided in <tt>web.xml</tt></td></tr>
040      <tr><td><tt> Locale </tt></td><td><tt> ResultSet.getString(), {@link hirondelle.web4j.util.Util#buildLocale(String)}</tt></td></tr>
041      <tr><td><tt> TimeZone </tt></td><td><tt> ResultSet.getString(), {@link hirondelle.web4j.util.Util#buildTimeZone(String)}</tt></td></tr>
042      <tr><td><tt> InputStream</tt></td><td><tt> ResultSet.getBinaryStream()</tt></td></tr>
043     </table>
044     
045     <P>This implementation supports the same building block classes defined by another  
046     default implementation : {@link hirondelle.web4j.model.ConvertParamImpl#isSupported(Class)}.
047     See that class for important information on the conditional support of <tt>String</tt>.
048    */
049    public class ConvertColumnImpl implements ConvertColumn {
050    
051      /**
052       Defines policies for converting a column of a <tt>ResultSet</tt> into a possibly-null
053       <tt>Object</tt>.
054      
055       @param aRow of a <tt>ResultSet</tt>
056       @param aColumnIdx particular column of aRow
057       @param aSupportedTargetType is a class supported by the configured implementation of 
058       {@link ConvertParam#isSupported(Class)}.
059      */
060      public <T> T convert(ResultSet aRow, int aColumnIdx, Class<T> aSupportedTargetType) throws SQLException {
061        if( ! fConvertParam.isSupported(aSupportedTargetType) ){
062          throw new IllegalArgumentException("Unsupported Target Type : " + Util.quote(aSupportedTargetType)); 
063        }
064        
065        Object result = null;
066        if (aSupportedTargetType == SafeText.class){
067          result = convertToSafeText(aRow, aColumnIdx);
068        }
069        else if (aSupportedTargetType == String.class) {
070          result = convertToString(aRow, aColumnIdx);
071        }
072        else if (aSupportedTargetType == Integer.class ||  aSupportedTargetType == int.class){
073          int value = aRow.getInt(aColumnIdx);
074          result = aRow.wasNull() ? null : new Integer(value);
075        }
076        else if (aSupportedTargetType == Boolean.class || aSupportedTargetType == boolean.class){
077          boolean value = aRow.getBoolean(aColumnIdx);
078          result = aRow.wasNull() ? null : Boolean.valueOf(value);
079        }
080        else if (aSupportedTargetType == BigDecimal.class){
081          result =  aRow.getBigDecimal(aColumnIdx);
082        }
083        else if (aSupportedTargetType == java.util.Date.class){
084          result = getDate(aRow, aColumnIdx);
085        }
086        else if (aSupportedTargetType == DateTime.class){
087          result = getDateTime(aRow, aColumnIdx);
088        }
089        else if (aSupportedTargetType == Long.class || aSupportedTargetType == long.class){
090          long value = aRow.getLong(aColumnIdx);
091          result = aRow.wasNull() ? null : new Long(value);
092        }
093        else if (aSupportedTargetType == Id.class){
094          String value = aRow.getString(aColumnIdx);
095          result = aRow.wasNull() ? null : new Id(value);
096        }
097        else if (aSupportedTargetType == Locale.class){
098          String value = aRow.getString(aColumnIdx);
099          result = value == null ? null : Util.buildLocale(value);
100        }
101        else if (TimeZone.class == aSupportedTargetType){
102          String value = aRow.getString(aColumnIdx);
103          result = value == null ? null : Util.buildTimeZone(value);
104        }
105        else if (aSupportedTargetType == Decimal.class){
106          BigDecimal value =  aRow.getBigDecimal(aColumnIdx);
107          result = value == null ? null : new Decimal(value);
108        }
109        else if (InputStream.class.isAssignableFrom(aSupportedTargetType)){
110          InputStream value =  aRow.getBinaryStream(aColumnIdx);
111          result = value == null ? null : value; 
112        }
113        else {
114          throw new AssertionError(
115            "Unsupported type cannot be translated to an object:" + aSupportedTargetType
116          );
117        }
118        fLogger.finest(
119          "Successfully converted ResultSet column idx " + Util.quote(aColumnIdx) + 
120          " into a " + aSupportedTargetType.getName() 
121        );
122        return (T)result; //this cast is unavoidable, and safe.
123      }
124    
125      // PRIVATE 
126      private final ConvertParam fConvertParam = BuildImpl.forConvertParam();
127      private static final Logger fLogger = Util.getLogger(ConvertColumnImpl.class);
128      
129      private SafeText convertToSafeText(ResultSet aRow, int aColumnIdx) throws SQLException {
130        String result = convertToString(aRow, aColumnIdx);
131        return result == null ? null : new SafeText(result);
132      }
133      
134      private String convertToString(ResultSet aRow, int aColumnIdx) throws SQLException {
135        String result = null;
136        if ( isClob(aRow, aColumnIdx) ) {
137          result = convertClobToString(aRow, aColumnIdx);
138        }
139        else {
140          result = aRow.getString(aColumnIdx);
141        }
142        return aRow.wasNull() ? null : result;
143      }
144      
145      private static boolean isClob(ResultSet aRow, int aColumnIdx) throws SQLException {
146        ResultSetMetaData metaData = aRow.getMetaData();
147        boolean result = metaData.getColumnType(aColumnIdx) == Types.CLOB;
148        return result;
149      }
150    
151      private static String convertClobToString(ResultSet aRow, int aColumnIdx) throws SQLException {
152        String result = null;
153        Clob clob = aRow.getClob(aColumnIdx);
154        if (clob != null){
155          int length = new Long(clob.length()).intValue();
156          result = clob.getSubString(1, length);
157        }
158        return result;
159      }
160      
161      private java.util.Date getDate(ResultSet aRow, int aColumnIdx) throws SQLException {
162        java.util.Date result = null;
163        Config config = new Config();
164        if ( config.hasTimeZoneHint() ){
165          result = aRow.getTimestamp(aColumnIdx, config.getTimeZoneHint());
166        }
167        else {
168          result = aRow.getTimestamp(aColumnIdx);
169        }
170        return result;
171      }
172      
173      private DateTime getDateTime(ResultSet aRow, int aColumnIdx) throws SQLException {
174        //the ctor takes any String; if malformed, the DateTime will blow up later, when you call most other methods
175        String rawDateTime = aRow.getString(aColumnIdx);
176        if(fLogger.getLevel() == Level.FINEST){
177          fLogger.finest("DateTime column, raw value from database is: " + Util.quote(rawDateTime) + 
178           ". SQL date-time formatting functions can be used to render the format compatible with the hirondelle.web4.model.DateTime class, if needed."
179          );
180        }
181        return rawDateTime == null ? null :  new DateTime(rawDateTime);
182      }
183    }