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    }