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