package hirondelle.web4j.model;
import java.math.BigDecimal;
import java.util.*;
import java.util.logging.Logger;
import java.util.regex.Pattern;
import javax.servlet.ServletConfig;
import hirondelle.web4j.util.Util;
import hirondelle.web4j.BuildImpl;
import hirondelle.web4j.readconfig.InitParam;
import hirondelle.web4j.request.DateConverter;
import hirondelle.web4j.request.Formats;
import hirondelle.web4j.security.SafeText;
import sun.util.calendar.ZoneInfo;
/** Default implementation of {@link ConvertParam}.*/
public class ConvertParamImpl implements ConvertParam {
/** Called by the framework upon startup. */
public static void init(ServletConfig aConfig){
fIgnorableParamValue = fIGNORABLE_PARAM_VALUE.fetch(aConfig).getValue();
fAllowString = Util.parseBoolean(fALLOW_STRING.fetch(aConfig).getValue());
fSUPPORTED_CLASSES = new ArrayList<Class<?>>();
fSUPPORTED_CLASSES.add(Integer.class);
fSUPPORTED_CLASSES.add(int.class);
fSUPPORTED_CLASSES.add(Boolean.class);
fSUPPORTED_CLASSES.add(boolean.class);
fSUPPORTED_CLASSES.add(BigDecimal.class);
fSUPPORTED_CLASSES.add(java.util.Date.class);
fSUPPORTED_CLASSES.add(Long.class);
fSUPPORTED_CLASSES.add(long.class);
fSUPPORTED_CLASSES.add(Id.class);
fSUPPORTED_CLASSES.add(SafeText.class);
fSUPPORTED_CLASSES.add(Locale.class);
fSUPPORTED_CLASSES.add(TimeZone.class);
fSUPPORTED_CLASSES.add(ZoneInfo.class); //Cheating - used to id TimeZones
fSUPPORTED_CLASSES.add(Decimal.class);
fSUPPORTED_CLASSES.add(DateTime.class);
if(fAllowString){
fSUPPORTED_CLASSES.add(String.class);
}
fLogger.fine("Supported Classes : " + Util.logOnePerLine(fSUPPORTED_CLASSES));
}
/**
Return <tt>true</tt> only if <tt>aTargetClass</tt> is supported by this implementation.
<P>
The following classes are supported by this implementation as building block classes :
<ul>
<li><tt>{@link SafeText}</tt>
<li><tt>String</tt> (conditionally, see below)
<li><tt>Integer</tt>
<li><tt>Long</tt>
<li><tt>Boolean</tt>
<li><tt>BigDecimal</tt>
<li><tt>{@link Decimal}</tt>
<li><tt>{@link Id}</tt>
<li><tt>{@link DateTime}</tt>
<li><tt>java.util.Date</tt>
<li><tt>Locale</tt>
<li><tt>TimeZone</tt> and <tt>sun.util.calendar.ZoneInfo</tt> (which is <i>cheating</i> - see below).
</ul>
<P><i>You are not obliged to use this class to model Locale and TimeZone.
Many will choose to implement them as just another
<a href='http://www.web4j.com/UserGuide.jsp#StartupTasksAndCodeTables'>code table</a>
instead.</i> In this case, your model object constructors would usually take an {@link Id} parameter for these
items, and translate them into a {@link Code}. See the example apps for a demonstration of this technique.
<P>The <tt>TimeZone</tt> class is a problem here, since it's abstract. In fact, <b>this demonstrates a defect in the {@link ConvertParam}
interface itself</b> - it should take objects themselves, instead of classes. That would allow more flexible checks on <i>type</i> as
opposed to concrete <i>class</i>. In this implementation, {@link ZoneInfo} is hard-coded as the representative of all {@link TimeZone}
objects. This is poor style, since <tt>ZoneInfo</tt> is public, but "unpublished" by Sun - they may change to some other
class in the future.
<P><b>String is supported only when explicitly allowed.</b>
The <tt>AllowStringAsBuildingBlock</tt> setting in <tt>web.xml</tt>
controls whether or not this class allows <tt>String</tt> as a supported class.
By default, its value is <tt>FALSE</tt>, since {@link SafeText} is the recommended
replacement for <tt>String</tt>.
*/
public final boolean isSupported(Class<?> aTargetClass){
return fSUPPORTED_CLASSES.contains(aTargetClass);
}
/**
Coerce all parameters with no visible content to <tt>null</tt>.
<P>In addition, any raw input value that matches <tt>IgnorableParamValue</tt> in <tt>web.xml</tt> is
also coerced to <tt>null</tt>. See <tt>web.xml</tt> for more information.
<P>Any non-<tt>null</tt> result is trimmed.
This method can be overridden, if desired.
*/
public String filter(String aRawInputValue){
String result = aRawInputValue;
if ( ! Util.textHasContent(aRawInputValue) || aRawInputValue.equals(getIgnorableParamValue()) ){
result = null;
}
return Util.trimPossiblyNull(result); //some apps may elect to trim elsewhere
}
/**
Apply reasonable parsing policies, suitable for most applications.
<P>Roughly, the policies are :
<ul>
<li><tt>SafeText</tt> uses {@link SafeText#SafeText(String)}
<li><tt>String</tt> just return the filtered value as is
<li><tt>Integer</tt> uses {@link Integer#Integer(String)}
<li><tt>BigDecimal</tt> uses {@link Formats#getDecimalInputFormat()}
<li><tt>Decimal</tt> uses {@link Formats#getDecimalInputFormat()}
<li><tt>Boolean</tt> uses {@link Util#parseBoolean(String)}
<li><tt>DateTime</tt> uses {@link DateConverter#parseEyeFriendlyDateTime(String, Locale)}
and {@link DateConverter#parseHandFriendlyDateTime(String, Locale)}
<li><tt>Date</tt> uses {@link DateConverter#parseEyeFriendly(String, Locale, TimeZone)}
and {@link DateConverter#parseHandFriendly(String, Locale, TimeZone)}
<li><tt>Long</tt> uses {@link Long#Long(String)}
<li><tt>Id</tt> uses {@link Id#Id(String)}
<li><tt>Locale</tt> uses {@link Locale#getAvailableLocales()} and {@link Locale#toString()}, case sensitive.
<li><tt>TimeZone</tt> uses {@link TimeZone#getAvailableIDs()}, case sensitive.
</ul>
*/
public final <T> T convert(String aFilteredInputValue, Class<T> aSupportedTargetClass, Locale aLocale, TimeZone aTimeZone) throws ModelCtorException {
// Defensive : this check should have already been performed by the calling framework class.
if( ! isSupported(aSupportedTargetClass) ) {
throw new AssertionError("Unsupported type cannot be translated to an object: " + aSupportedTargetClass + ". If you're trying to use String, consider using SafeText instead. Otherwise, change the AllowStringAsBuildingBlock setting in web.xml.");
}
Object result = null;
if (aSupportedTargetClass == SafeText.class){
//no translation needed; some impl's might trim here, or force CAPS
result = parseSafeText(aFilteredInputValue);
}
else if (aSupportedTargetClass == String.class) {
result = aFilteredInputValue; //no translation needed; some impl's might trim here, or force CAPS
}
else if (aSupportedTargetClass == Integer.class || aSupportedTargetClass == int.class){
result = parseInteger(aFilteredInputValue);
}
else if (aSupportedTargetClass == Boolean.class || aSupportedTargetClass == boolean.class){
result = Util.parseBoolean(aFilteredInputValue);
}
else if (aSupportedTargetClass == BigDecimal.class){
result = parseBigDecimal(aFilteredInputValue, aLocale, aTimeZone);
}
else if (aSupportedTargetClass == Decimal.class){
result = parseDecimal(aFilteredInputValue, aLocale, aTimeZone);
}
else if (aSupportedTargetClass == java.util.Date.class){
result = parseDate(aFilteredInputValue, aLocale, aTimeZone);
}
else if (aSupportedTargetClass == DateTime.class){
result = parseDateTime(aFilteredInputValue, aLocale);
}
else if (aSupportedTargetClass == Long.class || aSupportedTargetClass == long.class){
result = parseLong(aFilteredInputValue);
}
else if (aSupportedTargetClass == Id.class){
result = new Id(aFilteredInputValue.trim());
}
else if (aSupportedTargetClass == Locale.class){
result = parseLocale(aFilteredInputValue);
}
else if (aSupportedTargetClass == TimeZone.class){
result = parseTimeZone(aFilteredInputValue);
}
else {
throw new AssertionError("Failed to build object for ostensibly supported class: " + aSupportedTargetClass);
}
fLogger.finer("Converted request param into a " + aSupportedTargetClass.getName());
return (T)result; //this cast is unavoidable, and safe.
}
/**
Return the <tt>IgnorableParamValue</tt> configured in <tt>web.xml</tt>.
See <tt>web.xml</tt> for more information.
*/
public static final String getIgnorableParamValue(){
return fIgnorableParamValue;
}
// PRIVATE
private static List<Class<?>> fSUPPORTED_CLASSES;
/**
Possible values include
<ul>
<li> null, denoting that all param values are to be accepted
<li> an empty String, corresponding to a blank OPTION
<li> '---SELECT---', for example
</ul>
*/
private static String fIgnorableParamValue;
private static final InitParam fIGNORABLE_PARAM_VALUE = new InitParam("IgnorableParamValue", "");
private static boolean fAllowString;
private static final InitParam fALLOW_STRING = new InitParam("AllowStringAsBuildingBlock", "NO");
private static final ModelCtorException PROBLEM_FOUND = new ModelCtorException();
private static final Logger fLogger = Util.getLogger(ConvertParamImpl.class);
private Integer parseInteger(String aUserInputValue) throws ModelCtorException {
try {
return new Integer(aUserInputValue);
}
catch (NumberFormatException ex){
throw PROBLEM_FOUND;
}
}
private BigDecimal parseBigDecimal(String aUserInputValue, Locale aLocale, TimeZone aTimeZone) throws ModelCtorException {
BigDecimal result = null;
Formats formats = new Formats(aLocale, aTimeZone);
Pattern pattern = formats.getDecimalInputFormat();
if ( Util.matches(pattern, aUserInputValue)) {
//BigDecimal ctor only takes '.' as decimal sign, never ','
result = new BigDecimal(aUserInputValue.replace(',', '.'));
}
else {
throw PROBLEM_FOUND;
}
return result;
}
private Decimal parseDecimal(String aUserInputValue, Locale aLocale, TimeZone aTimeZone) throws ModelCtorException {
Decimal result = null;
BigDecimal amount = null;
Formats formats = new Formats(aLocale, aTimeZone);
Pattern pattern = formats.getDecimalInputFormat();
if ( Util.matches(pattern, aUserInputValue)) {
//BigDecimal ctor only takes '.' as decimal sign, never ','
amount = new BigDecimal(aUserInputValue.replace(',', '.'));
try {
result = new Decimal(amount);
}
catch(IllegalArgumentException ex){
throw PROBLEM_FOUND;
}
}
else {
throw PROBLEM_FOUND;
}
return result;
}
private Date parseDate(String aUserInputValue, Locale aLocale, TimeZone aTimeZone) throws ModelCtorException {
Date result = null;
DateConverter dateConverter = BuildImpl.forDateConverter();
result = dateConverter.parseHandFriendly(aUserInputValue, aLocale, aTimeZone);
if ( result == null ){
result = dateConverter.parseEyeFriendly(aUserInputValue, aLocale, aTimeZone);
}
if ( result == null ) {
throw PROBLEM_FOUND;
}
return result;
}
private DateTime parseDateTime(String aUserInputValue, Locale aLocale) throws ModelCtorException {
DateTime result = null;
DateConverter dateConverter = BuildImpl.forDateConverter();
result = dateConverter.parseHandFriendlyDateTime(aUserInputValue, aLocale);
if ( result == null ){
result = dateConverter.parseEyeFriendlyDateTime(aUserInputValue, aLocale);
}
if ( result == null ) {
throw PROBLEM_FOUND;
}
return result;
}
private Long parseLong(String aUserInputValue) throws ModelCtorException {
Long result = null;
if ( Util.textHasContent(aUserInputValue) ){
try {
result = new Long(aUserInputValue);
}
catch (NumberFormatException ex){
throw PROBLEM_FOUND;
}
}
return result;
}
private SafeText parseSafeText(String aUserInputValue) throws ModelCtorException {
SafeText result = null;
if( Util.textHasContent(aUserInputValue) ) {
try {
result = new SafeText(aUserInputValue);
}
catch(IllegalArgumentException ex){
throw PROBLEM_FOUND;
}
}
return result;
}
/** Translate user input into a known time zone id. Case sensitive. */
private TimeZone parseTimeZone(String aUserInputValue) throws ModelCtorException {
TimeZone result = null;
if ( Util.textHasContent(aUserInputValue) ){
List<String> allTimeZoneIds = Arrays.asList(TimeZone.getAvailableIDs());
for(String id : allTimeZoneIds){
if (id.equals(aUserInputValue)){
result = TimeZone.getTimeZone(id);
break;
}
}
if(result == null){ //has content, but no match found
throw PROBLEM_FOUND;
}
}
return result;
}
/** Translate user input into a known Locale id. Case sensitive. */
private Locale parseLocale(String aUserInputValue) throws ModelCtorException {
Locale result = null;
if ( Util.textHasContent(aUserInputValue) ){
List<Locale> allLocales = Arrays.asList(Locale.getAvailableLocales());
for(Locale locale: allLocales){
if (locale.toString().equals(aUserInputValue)){
result = locale;
break;
}
}
if(result == null){ //has content, but no match found
throw PROBLEM_FOUND;
}
}
return result;
}
}