001// --------------------------------------------------------------------------------
002// Copyright 2002-2024 Echo Three, LLC
003//
004// Licensed under the Apache License, Version 2.0 (the "License");
005// you may not use this file except in compliance with the License.
006// You may obtain a copy of the License at
007//
008//     http://www.apache.org/licenses/LICENSE-2.0
009//
010// Unless required by applicable law or agreed to in writing, software
011// distributed under the License is distributed on an "AS IS" BASIS,
012// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
013// See the License for the specific language governing permissions and
014// limitations under the License.
015// --------------------------------------------------------------------------------
016
017package com.echothree.util.server.validation;
018
019import com.echothree.model.control.accounting.server.control.AccountingControl;
020import com.echothree.model.control.party.server.control.PartyControl;
021import com.echothree.model.control.user.server.control.UserControl;
022import com.echothree.model.control.vendor.server.control.VendorControl;
023import com.echothree.model.data.accounting.server.entity.Currency;
024import com.echothree.util.common.string.StringUtils;
025import com.echothree.util.common.validation.FieldDefinition;
026import com.echothree.util.common.validation.FieldType;
027import com.echothree.util.common.form.BaseForm;
028import com.echothree.util.common.form.ValidationResult;
029import com.echothree.util.common.message.Message;
030import com.echothree.util.common.message.Messages;
031import com.echothree.util.common.transfer.Limit;
032import com.echothree.util.server.control.BaseCommand;
033import com.echothree.util.server.persistence.Session;
034import com.echothree.util.server.validation.fieldtype.*;
035import com.google.common.base.Splitter;
036import java.lang.reflect.Constructor;
037import java.lang.reflect.InvocationTargetException;
038import java.util.Collections;
039import java.util.HashMap;
040import java.util.List;
041import java.util.Map;
042import java.util.Set;
043import java.util.regex.Matcher;
044import org.apache.commons.logging.Log;
045import org.apache.commons.logging.LogFactory;
046
047public class Validator {
048    
049    private final BaseCommand baseCommand;
050    private AccountingControl accountingControl = null;
051    private PartyControl partyControl = null;
052    private UserControl userControl = null;
053    private VendorControl vendorControl = null;
054    private Log log = null;
055    
056    private Currency currency = null;
057    
058    public static final String ERROR_REQUIRED_FIELD           = "RequiredField";
059    public static final String ERROR_MINIMUM_LENGTH           = "MinimumLength";
060    public static final String ERROR_MAXIMUM_LENGTH           = "MaximumLength";
061    public static final String ERROR_MINIMUM_VALUE            = "MinimumValue";
062    public static final String ERROR_MAXIMUM_VALUE            = "MaximumValue";
063    public static final String ERROR_NO_VALUE_ALLOWED         = "NoValueAllowed";
064    public static final String ERROR_UNKOWN_CURRENCY_ISO_NAME = "UnknownCurrencyIsoName";
065    public static final String ERROR_UNKOWN_VENDOR_NAME       = "UnknownVendorName";
066    public static final String ERROR_INTERNAL_ERROR           = "InternalError";
067    public static final String ERROR_INVALID_FORMAT           = "InvalidFormat";
068    public static final String ERROR_INVALID_OPTION           = "InvalidOption";
069    public static final String ERROR_INVALID_LIMIT            = "InvalidLimit";
070    
071    public static final Map<FieldType, Class> fieldTypes;
072    
073    static {
074        Map<FieldType, Class> map = new HashMap<>(44);
075        
076        map.put(FieldType.BOOLEAN, BooleanFieldType.class);
077        map.put(FieldType.COMMAND_NAME, CommandNameFieldType.class);
078        map.put(FieldType.COST_LINE, CostLineFieldType.class);
079        map.put(FieldType.COST_UNIT, CostUnitFieldType.class);
080        map.put(FieldType.CREDIT_CARD_MONTH, CreditCardMonthFieldType.class);
081        map.put(FieldType.CREDIT_CARD_YEAR, CreditCardYearFieldType.class);
082        map.put(FieldType.DATE, DateFieldType.class);
083        map.put(FieldType.DATE_TIME, DateTimeFieldType.class);
084        map.put(FieldType.EMAIL_ADDRESS, EmailAddressFieldType.class);
085        map.put(FieldType.ENTITY_NAME, EntityNameFieldType.class);
086        map.put(FieldType.ENTITY_NAME2, EntityName2FieldType.class);
087        map.put(FieldType.ENTITY_NAMES, EntityNamesFieldType.class);
088        map.put(FieldType.ENTITY_REF, EntityRefFieldType.class);
089        map.put(FieldType.ENTITY_TYPE_NAME, EntityTypeNameFieldType.class);
090        map.put(FieldType.FRACTIONAL_PERCENT, FractionalPercentFieldType.class);
091        map.put(FieldType.GUID, GuidFieldType.class);
092        map.put(FieldType.HARMONIZED_TARIFF_SCHEDULE_CODE, HarmonizedTariffScheduleCodeFieldType.class);
093        map.put(FieldType.HOST_NAME, HostNameFieldType.class);
094        map.put(FieldType.ID, IdFieldType.class);
095        map.put(FieldType.INET_4_ADDRESS, Inet4AddressFieldType.class);
096        map.put(FieldType.KEY, KeyFieldType.class);
097        map.put(FieldType.LATITUDE, LatitudeFieldType.class);
098        map.put(FieldType.LONGITUDE, LongitudeFieldType.class);
099        map.put(FieldType.MIME_TYPE, MimeTypeFieldType.class);
100        map.put(FieldType.NULL, NullFieldType.class);
101        map.put(FieldType.NUMBER_3, Number3FieldType.class);
102        map.put(FieldType.NUMBERS, NumbersFieldType.class);
103        map.put(FieldType.PRICE_LINE, PriceLineFieldType.class);
104        map.put(FieldType.PRICE_UNIT, PriceUnitFieldType.class);
105        map.put(FieldType.REGULAR_EXPRESSION, RegularExpressionFieldType.class);
106        map.put(FieldType.SEQUENCE_MASK, SequenceMaskFieldType.class);
107        map.put(FieldType.SIGNED_INTEGER, SignedIntegerFieldType.class);
108        map.put(FieldType.SIGNED_LONG, SignedLongFieldType.class);
109        map.put(FieldType.STRING, StringFieldType.class);
110        map.put(FieldType.TAG, TagFieldType.class);
111        map.put(FieldType.TIME_ZONE_NAME, TimeZoneNameFieldType.class);
112        map.put(FieldType.UNSIGNED_COST_LINE, UnsignedCostLineFieldType.class);
113        map.put(FieldType.UNSIGNED_COST_UNIT, UnsignedCostUnitFieldType.class);
114        map.put(FieldType.UNSIGNED_INTEGER, UnsignedIntegerFieldType.class);
115        map.put(FieldType.UNSIGNED_LONG, UnsignedLongFieldType.class);
116        map.put(FieldType.UNSIGNED_PRICE_LINE, UnsignedPriceLineFieldType.class);
117        map.put(FieldType.UNSIGNED_PRICE_UNIT, UnsignedPriceUnitFieldType.class);
118        map.put(FieldType.UPPER_LETTER_2, UpperLetter2FieldType.class);
119        map.put(FieldType.UPPER_LETTER_3, UpperLetter3FieldType.class);
120        map.put(FieldType.ULID, UlidFieldType.class);
121        map.put(FieldType.URL, UrlFieldType.class);
122        map.put(FieldType.YEAR, YearFieldType.class);
123        
124        fieldTypes = Collections.unmodifiableMap(map);
125    }
126    
127    /** Creates a new instance of Validator */
128    public Validator(BaseCommand baseCommand) {
129        this.baseCommand = baseCommand;
130    }
131    
132    public BaseCommand getBaseCommand() {
133        return baseCommand;
134    }
135
136    public AccountingControl getAccountingControl() {
137        if(accountingControl == null)
138            accountingControl = Session.getModelController(AccountingControl.class);
139        return accountingControl;
140    }
141    
142    public PartyControl getPartyControl() {
143        if(partyControl == null)
144            partyControl = Session.getModelController(PartyControl.class);
145        return partyControl;
146    }
147    
148    public UserControl getUserControl() {
149        if(userControl == null)
150            userControl = Session.getModelController(UserControl.class);
151        return userControl;
152    }
153    
154    public VendorControl getVendorControl() {
155        if(vendorControl == null)
156            vendorControl = Session.getModelController(VendorControl.class);
157        return vendorControl;
158    }
159    
160    protected Log getLog() {
161        if(log == null) {
162            log = LogFactory.getLog(this.getClass());
163        }
164        
165        return log;
166    }
167    
168    public Currency getCurrency() {
169        return currency;
170    }
171
172    public void setCurrency(Currency currency) {
173        this.currency = currency;
174    }
175
176    /**
177     * Returns An ArrayList if there are errors found, otherwise null.
178     * @return An ArrayList if there are errors found, otherwise null
179     */
180    public Messages validateField(BaseForm form, FieldDefinition fieldDefinition) {
181        Messages validationMessages = new Messages();
182        String []splitFieldName = Splitter.on(':').trimResults().omitEmptyStrings().splitToList(fieldDefinition.getFieldName()).toArray(new String[0]);
183        String fieldName = splitFieldName[0];
184        String originalFieldValue = form == null ? null : (String)form.get(fieldName);
185        String fieldValue = originalFieldValue;
186        
187        // Clean the String.
188        fieldValue = StringUtils.getInstance().trimToNull(fieldValue);
189        
190        if(fieldValue == null) {
191            // If its null, and its a required field, then its an error.
192            if(fieldDefinition.getIsRequired()) {
193                validationMessages.add(fieldName, new Message(ERROR_REQUIRED_FIELD));
194            }
195        } else {
196            FieldType fieldType = fieldDefinition.getFieldType();
197            Class fieldValidator = fieldTypes.get(fieldType);
198            
199            // Not all fieldTypes have an additional validator class
200            if(fieldValidator != null) {
201                try {
202                    Constructor constructor = fieldValidator.getConstructor(new Class[]{Validator.class, BaseForm.class, Messages.class, String.class, String [].class, FieldDefinition.class});
203                    BaseFieldType baseFieldType = (BaseFieldType)constructor.newInstance(new Object[]{this, form, validationMessages, fieldValue, splitFieldName, fieldDefinition});
204
205                    fieldValue = baseFieldType.validate();
206                } catch (InstantiationException | NoSuchMethodException | IllegalAccessException | InvocationTargetException e) {
207                    //e.printStackTrace();
208                    //e.getCause().printStackTrace();
209                    validationMessages.add(fieldName, new Message(ERROR_INTERNAL_ERROR));
210                    fieldValue = null;
211                    //System.err.println("Validator.validateField: fieldName = " + fieldName + ", Exception");
212                }
213            }
214        }
215        
216        // Put back the value we've cleaned up.
217        if(form != null) {
218            form.set(fieldName, fieldValue);
219        }
220        
221        if(!validationMessages.isEmpty()) {
222            getLog().info("formName = " + form == null ? null : form.getFormName() + ", fieldName = " + fieldName + ", originalFieldValue = \""
223                    + originalFieldValue + "\", fieldValue = \"" + fieldValue + "\", errorList = " + validationMessages);
224        }
225        
226        return validationMessages.isEmpty()? null: validationMessages;
227    }
228    
229    private final static FieldDefinition preferredClobMimeTypeNameFieldDefinition = new FieldDefinition("PreferredClobMimeTypeName", FieldType.MIME_TYPE, Boolean.FALSE, null, null);
230    
231    public void validatePreferredClobMimeTypeName(Messages formValidationMessages, BaseForm form) {
232        Messages validationMessages = validateField(form, preferredClobMimeTypeNameFieldDefinition);
233
234        if(validationMessages != null) {
235            formValidationMessages.add(validationMessages);
236        }
237    }
238    
239    public void validateOptions(Messages formValidationMessages, BaseForm form) {
240        Set<String> options = form.getOptions();
241
242        if(options != null) {
243            options.forEach((option) -> {
244                Matcher m = Patterns.Option.matcher(option);
245                if (!m.matches()) {
246                    formValidationMessages.add(option, new Message(Validator.ERROR_INVALID_OPTION));
247                }
248            });
249        }
250    }
251    
252    public static String validateLong(String fieldValue) {
253        try {
254            Long testLong = Long.valueOf(fieldValue);
255            
256            fieldValue = testLong.toString();
257        } catch (NumberFormatException nfe) {
258            if(fieldValue.equalsIgnoreCase("MAX_VALUE")) {
259                fieldValue = Long.toString(Long.MAX_VALUE);
260            } else {
261                fieldValue = null;
262            }
263        }
264        
265        return fieldValue;
266    }
267    
268    public static String validateUnsignedLong(String unsignedLong) {
269        if(unsignedLong != null) {
270            Matcher m = Patterns.UnsignedNumbers.matcher(unsignedLong);
271
272            if(m.matches()) {
273                unsignedLong = validateLong(unsignedLong);
274            } else {
275                unsignedLong = null;
276            }
277        }
278        
279        return unsignedLong;
280    }
281    
282    public void validateLimits(Messages formValidationMessages, BaseForm form) {
283        Map<String, Limit> limits = form.getLimits();
284
285        if(limits != null) {
286            limits.keySet().stream().forEach((tableNameSingular) -> {
287                boolean validLimit = true;
288                Matcher m = Patterns.TableNameSingular.matcher(tableNameSingular);
289                if(m.matches()) {
290                    Limit limit = limits.get(tableNameSingular);
291
292                    if(limit != null) {
293                        String count = limit.getCount();
294                        String newCount = count == null ? null : validateUnsignedLong(count);
295
296                        if(count == null || (count != null && newCount != null)) {
297                            limit.setCount(newCount);
298                        } else {
299                            validLimit = false;
300                        }
301
302                        if(validLimit) {
303                            String offset = limit.getOffset();
304                            String newOffset = offset == null ? null : validateUnsignedLong(offset);
305
306                            if(offset == null || (offset != null && newOffset != null)) {
307                                limit.setOffset(newOffset);
308                            } else {
309                                validLimit = false;
310                            }
311                        }
312                    }
313                } else {
314                    validLimit = false;
315                }
316                if (!validLimit) {
317                    formValidationMessages.add(tableNameSingular, new Message(Validator.ERROR_INVALID_LIMIT));
318                }
319            });
320        }
321    }
322    
323    public ValidationResult validate(BaseForm form, List<FieldDefinition> fieldDefinitions) {
324        Messages formValidationMessages = new Messages();
325        
326        fieldDefinitions.stream().map((fieldDefinition) -> validateField(form, fieldDefinition)).filter((validationMessages) -> (validationMessages != null)).forEach((validationMessages) -> {
327            formValidationMessages.add(validationMessages);
328        });
329        
330        if(form != null) {
331            validatePreferredClobMimeTypeName(formValidationMessages, form);
332            validateOptions(formValidationMessages, form);
333            validateLimits(formValidationMessages, form);
334        }
335        
336        boolean hasErrors = !formValidationMessages.isEmpty();
337        ValidationResult validationResult = new ValidationResult(hasErrors? formValidationMessages: null);
338        
339        return validationResult;
340    }
341
342}