001 package hirondelle.web4jtools.codegenerator.field; 002 003 import hirondelle.web4j.model.ModelCtorException; 004 import hirondelle.web4j.model.ModelUtil; 005 import hirondelle.web4jtools.util.Check; 006 import hirondelle.web4j.util.Util; 007 import static hirondelle.web4j.util.Consts.FAILS; 008 import hirondelle.web4jtools.codegenerator.codes.ControlStyle; 009 import hirondelle.web4jtools.codegenerator.codes.FieldType; 010 import hirondelle.web4jtools.codegenerator.codes.SortOrder; 011 import hirondelle.web4j.model.Id; 012 013 /** 014 * A data field used by the feature. 015 * 016 * <P>A data field usually maps to a database table column. 017 * 018 * <P>One of these fields must be marked as the primary key. 019 */ 020 public final class Field { 021 022 /** 023 * Constructor. 024 * 025 * <P>Here, the <tt>Id</tt> is not a database key. Rather, it is an index into a collection of Fields 026 * stored only in memory. It acts much as a primary key, however. 027 * 028 * <P><b>'Required' Requires Some Explanation</b><br> 029 * <span class='highlight'>Items are required in the sense of needing to be non-null in the Model Object. 030 * They are <em>not</em> required in the sense of being non-null in the database.</span> 031 * Usually this distinction is irrelevant. However, for primary keys the distinction is indeed relevant : primary keys are 032 * required in the database, but often <em>not</em> required in the Model Object. 033 * When adding a new record, a Model Object is first created, to model the user input. 034 * At this stage, however, the primary key is usually not yet defined. This is particularly true when the database 035 * autogenerates its primary keys when new records are added. 036 * 037 * <P><b>Minimum and Maximum</b><br> 038 * These are numeric entries, and are interpreted differently according to {@link FieldType}. For numeric data, they are simply 039 * limits on the field's value. For text data, they are limits on the number of characters. 040 * See {@link hirondelle.web4j.model.Check} for more information on its <tt>min</tt> and <tt>max</tt> methods. 041 * 042 * <P><b>Hard Validation</b><br> 043 * One and only one form of 044 * <a href='http://www.web4j.com/web4j/javadoc/hirondelle/web4j/security/ApplicationFirewall.html'>hard validation</a> 045 * must be selected, either <em>length</em> or <em>pattern</em>, but not both. 046 * 047 * <P><b>Soft Validation</b><br> 048 * See 049 * <a href='http://www.web4j.com/web4j/javadoc/hirondelle/web4j/security/ApplicationFirewall.html'>ApplicationFirewall</a> 050 * and 051 * <a href='http://www.web4j.com/web4j/javadoc/hirondelle/web4j/model/Check.html'>Check</a> for 052 * more information on soft validation. 053 * 054 * @param aId optional, since when adding new records the id is unknown. 055 * @param aName is required, 1..100 characters; enter as natural text, such as <tt>'Jet Engine'</tt>, with a space. 056 * @param aDescription is optional, 1..1000 characters. 057 * @param aIsRequired is optional; required fields are tested for non-nullity in the Model Object. 058 * @param aType is required, defines the {@link FieldType}, the java class used to represent the field 059 * @param aControlStyle is required, defines the {@link ControlStyle}, the HTML control used to enter the field value. 060 * @param aIsPrimaryKey is optional; only one field in the feature should be the primary key. 061 * @param aMinimum is optional, cannot be greater than <tt>aMaximum</tt>. 062 * @param aMaximum is optional, cannot be less than <tt>aMinimum</tt>. 063 * @param aNumDecimals is optional, integer, <tt>1</tt> or more. 064 * @param aErrorMessage is optional, the error message displayed when the user inputs an invalid value, 1..200 characters 065 * @param aIsHardValidatedForLength is optional. 066 * @param aHardValidationPattern is optional, regular expression, 1..200 characters. 067 * @param aCheckPattern is optional, regular expression, 1..200 characters. 068 * @param aCheckEmail is optional, <tt>true</tt> only if this field contains an email address. 069 * @param aCheckSpam is optional, <tt>true</tt> only if this is a text field that should be checked for spam. 070 * @param aIsOrderByField is optional, <tt>true</tt> only if this is the field by which listings should be sorted; only one 071 * field in the feature should be the ORDER BY field. 072 * @param aIsDescendingOrder is optional, and takes effect only if this is the ORDER BY field. 073 * @param aCodeTable is optional, and is the name of an application-scope code table; 074 * applies only if <tt>aControlStyle</tt> is <tt>SELECT</tt> or <tt>RADIO</tt>; 075 */ 076 public Field( 077 Id aId, String aName, String aDescription, Boolean aIsRequired, String aType, String aControlStyle, Boolean aIsPrimaryKey, 078 Integer aMinimum, Integer aMaximum, Integer aNumDecimals, String aErrorMessage, Boolean aIsHardValidatedForLength, 079 String aHardValidationPattern, String aCheckPattern, Boolean aCheckEmail, Boolean aCheckSpam, 080 Boolean aIsOrderByField, Boolean aIsDescendingOrder, String aCodeTable 081 ) throws ModelCtorException { 082 fId = aId; 083 fName = aName; 084 fDescription = aDescription; 085 fIsRequired = Util.nullMeansFalse(aIsRequired); 086 fType = Util.textHasContent(aType) ? FieldType.valueOf(aType) : null; 087 fControlStyle = Util.textHasContent(aControlStyle) ? ControlStyle.valueOf(aControlStyle) : null; 088 fIsPrimaryKey = Util.nullMeansFalse(aIsPrimaryKey); 089 fMinimum = aMinimum; 090 fMaximum = aMaximum; 091 fNumDecimals = aNumDecimals; 092 fErrorMessage = aErrorMessage; 093 fIsHardValidatedForLength = Util.nullMeansFalse(aIsHardValidatedForLength); 094 fHardValidationPattern = aHardValidationPattern; 095 fCheckPattern = aCheckPattern; 096 fCheckEmail = Util.nullMeansFalse(aCheckEmail); 097 fCheckSpam = Util.nullMeansFalse(aCheckSpam); 098 fIsOrderByField = Util.nullMeansFalse(aIsOrderByField); 099 fIsDescendingOrder = Util.nullMeansFalse(aIsDescendingOrder); 100 fCodeTable = aCodeTable; 101 validateState(); 102 } 103 104 public Id getId() { return fId; } 105 public String getName() { return fName; } 106 public String getDescription() { return fDescription; } 107 public Boolean getIsRequired() { return fIsRequired; } 108 public FieldType getType() { return fType; } 109 public ControlStyle getControlStyle() { return fControlStyle; } 110 public Boolean getIsPrimaryKey() { return fIsPrimaryKey; } 111 public Integer getMinimum() { return fMinimum; } 112 public Integer getMaximum() { return fMaximum; } 113 public Integer getNumDecimals(){ return fNumDecimals; } 114 public String getErrorMessage() { return fErrorMessage; } 115 public Boolean getIsHardValidatedForLength() { return fIsHardValidatedForLength; } 116 public String getHardValidationPattern() { return fHardValidationPattern; } 117 public String getCheckPattern() { return fCheckPattern; } 118 public Boolean getCheckEmail() { return fCheckEmail; } 119 public Boolean getCheckSpam() { return fCheckSpam; } 120 public Boolean getIsOrderByField() { return fIsOrderByField; } 121 public Boolean getIsDescendingOrder() { 122 return fIsOrderByField == null ? null : fIsDescendingOrder; 123 } 124 public String getCodeTable(){ return fCodeTable; } 125 126 /** 127 * Return the {@link SortOrder} calculated from {@link #getIsOrderByField()} and {@link #getIsDescendingOrder()}. 128 */ 129 public SortOrder getSortOrder() { 130 SortOrder result = null; 131 if( fIsOrderByField == true ) { 132 result = fIsDescendingOrder ? SortOrder.DESC : SortOrder.ASC; 133 } 134 return result; 135 } 136 137 /** Intended for debugging only. */ 138 @Override public String toString() { 139 return ModelUtil.toStringFor(this); 140 } 141 142 @Override public boolean equals( Object aThat ) { 143 Boolean result = ModelUtil.quickEquals(this, aThat); 144 if ( result == null ){ 145 Field that = (Field) aThat; 146 result = ModelUtil.equalsFor(this.getSignificantFields(), that.getSignificantFields()); 147 } 148 return result; 149 } 150 151 @Override public int hashCode() { 152 if ( fHashCode == 0 ) { 153 fHashCode = ModelUtil.hashCodeFor(getSignificantFields()); 154 } 155 return fHashCode; 156 } 157 158 // PRIVATE // 159 private final Id fId; 160 private final String fName; 161 private final String fDescription; 162 private final Boolean fIsRequired; 163 private final FieldType fType; 164 private final ControlStyle fControlStyle; 165 private final Boolean fIsPrimaryKey; 166 private final Integer fMinimum; 167 private final Integer fMaximum; 168 private final Integer fNumDecimals; 169 private final String fErrorMessage; 170 private final Boolean fIsHardValidatedForLength; 171 private final String fHardValidationPattern; 172 private final String fCheckPattern; 173 private final Boolean fCheckEmail; 174 private final Boolean fCheckSpam; 175 private final Boolean fIsOrderByField; 176 private final Boolean fIsDescendingOrder; 177 private final String fCodeTable; 178 179 private int fHashCode; 180 181 private void validateState() throws ModelCtorException { 182 ModelCtorException ex = new ModelCtorException(); 183 if ( FAILS == Check.required(fName, Check.range(1,100)) ) { 184 ex.add("Name required, 1..100 characters."); 185 } 186 if ( FAILS == Check.required(fType) ) { 187 ex.add("Type is required."); 188 } 189 if ( FAILS == Check.required(fControlStyle) ) { 190 ex.add("Control Style is required."); 191 } 192 if ( FAILS == Check.optional(fDescription, Check.range(1,1000))) { 193 ex.add("Description is optional, 1..1000 characters."); 194 } 195 if ( FAILS == Check.optional(fErrorMessage, Check.range(1,200)) ) { 196 ex.add("Message for invalid input is optional, 1..200 characters."); 197 } 198 if ( FAILS == Check.optional(fHardValidationPattern, Check.range(1,200), Check.isRegex()) ) { 199 ex.add("Hard-validation pattern optional, regular expression, 1..200 characters."); 200 } 201 if ( fIsHardValidatedForLength == true && Util.textHasContent(fHardValidationPattern)) { 202 ex.add("Hard validation for length and for pattern are mutually exclusive. Please select one or the other."); 203 } 204 if ( fIsHardValidatedForLength == false && ! Util.textHasContent(fHardValidationPattern)) { 205 ex.add("Please select a style of hard validation."); 206 } 207 if( FAILS == Check.optional(fCheckPattern, Check.isRegex(), Check.range(1,200))) { 208 ex.add("Soft Validation pattern is not a regular expression, 1..200 characters."); 209 } 210 if (fIsPrimaryKey == true && fIsRequired == true ) { 211 ex.add("For Model Objects, primary key cannot be required: 'Add' operations need an empty primary key."); 212 } 213 if ( fMinimum != null && fMaximum != null ) { 214 if( fMinimum.compareTo(fMaximum) > 0 ) { 215 ex.add("Minimum cannot be greater than maximum"); 216 } 217 } 218 if ( FAILS == Check.optional(fNumDecimals, Check.min(1))) { 219 ex.add("Number of decimals must be 1 or more."); 220 } 221 if( needsCodeTable(fControlStyle) && ! Util.textHasContent(fCodeTable)) { 222 ex.add("Please enter a code table."); 223 } 224 if ( Util.textHasContent(fCodeTable) && ! needsCodeTable(fControlStyle)) { 225 ex.add("Please remove code table. Code tables are not used with this kind of control."); 226 } 227 if ( ex.isNotEmpty() ) { throw ex; } 228 } 229 230 private boolean needsCodeTable(ControlStyle aStyle){ 231 return (aStyle == ControlStyle.Select || aStyle == ControlStyle.Radio); 232 } 233 234 private Object[] getSignificantFields(){ 235 return new Object[]{ 236 fName, fDescription, fIsRequired, fType, fControlStyle, fIsPrimaryKey, fMinimum, fMaximum, fNumDecimals, 237 fErrorMessage, fIsHardValidatedForLength, fHardValidationPattern, fIsOrderByField, fIsDescendingOrder, 238 fCodeTable 239 }; 240 } 241 }