001// --------------------------------------------------------------------------------
002// Copyright 2002-2026 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.form.BaseForm;
025import com.echothree.util.common.form.ValidationResult;
026import com.echothree.util.common.message.Message;
027import com.echothree.util.common.message.Messages;
028import com.echothree.util.common.string.StringUtils;
029import com.echothree.util.common.validation.FieldDefinition;
030import com.echothree.util.common.validation.FieldType;
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.slf4j.Logger;
042import org.slf4j.LoggerFactory;
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    protected final Logger log = LoggerFactory.getLogger(this.getClass());
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.INITIALIZATION_VECTOR, InitializationVectorFieldType.class);
094        map.put(FieldType.KEY, KeyFieldType.class);
095        map.put(FieldType.LATITUDE, LatitudeFieldType.class);
096        map.put(FieldType.LONGITUDE, LongitudeFieldType.class);
097        map.put(FieldType.MIME_TYPE, MimeTypeFieldType.class);
098        map.put(FieldType.NULL, NullFieldType.class);
099        map.put(FieldType.NUMBER_3, Number3FieldType.class);
100        map.put(FieldType.NUMBERS, NumbersFieldType.class);
101        map.put(FieldType.PRICE_LINE, PriceLineFieldType.class);
102        map.put(FieldType.PRICE_UNIT, PriceUnitFieldType.class);
103        map.put(FieldType.REGULAR_EXPRESSION, RegularExpressionFieldType.class);
104        map.put(FieldType.SEQUENCE_MASK, SequenceMaskFieldType.class);
105        map.put(FieldType.SIGNED_INTEGER, SignedIntegerFieldType.class);
106        map.put(FieldType.SIGNED_LONG, SignedLongFieldType.class);
107        map.put(FieldType.STRING, StringFieldType.class);
108        map.put(FieldType.TAG, TagFieldType.class);
109        map.put(FieldType.TIME_ZONE_NAME, TimeZoneNameFieldType.class);
110        map.put(FieldType.UNSIGNED_COST_LINE, UnsignedCostLineFieldType.class);
111        map.put(FieldType.UNSIGNED_COST_UNIT, UnsignedCostUnitFieldType.class);
112        map.put(FieldType.UNSIGNED_INTEGER, UnsignedIntegerFieldType.class);
113        map.put(FieldType.UNSIGNED_LONG, UnsignedLongFieldType.class);
114        map.put(FieldType.UNSIGNED_PRICE_LINE, UnsignedPriceLineFieldType.class);
115        map.put(FieldType.UNSIGNED_PRICE_UNIT, UnsignedPriceUnitFieldType.class);
116        map.put(FieldType.UPPER_LETTER_2, UpperLetter2FieldType.class);
117        map.put(FieldType.UPPER_LETTER_3, UpperLetter3FieldType.class);
118        map.put(FieldType.URL, UrlFieldType.class);
119        map.put(FieldType.YEAR, YearFieldType.class);
120        
121        fieldTypes = Collections.unmodifiableMap(map);
122    }
123    
124    /** Creates a new instance of Validator */
125    public Validator(BaseCommand baseCommand) {
126        this.baseCommand = baseCommand;
127    }
128    
129    public BaseCommand getBaseCommand() {
130        return baseCommand;
131    }
132
133    public AccountingControl getAccountingControl() {
134        if(accountingControl == null)
135            accountingControl = Session.getModelController(AccountingControl.class);
136        return accountingControl;
137    }
138    
139    public PartyControl getPartyControl() {
140        if(partyControl == null)
141            partyControl = Session.getModelController(PartyControl.class);
142        return partyControl;
143    }
144    
145    public UserControl getUserControl() {
146        if(userControl == null)
147            userControl = Session.getModelController(UserControl.class);
148        return userControl;
149    }
150    
151    public VendorControl getVendorControl() {
152        if(vendorControl == null)
153            vendorControl = Session.getModelController(VendorControl.class);
154        return vendorControl;
155    }
156    
157    public Currency getCurrency() {
158        return currency;
159    }
160
161    public void setCurrency(Currency currency) {
162        this.currency = currency;
163    }
164
165    /**
166     * Returns An ArrayList if there are errors found, otherwise null.
167     * @return An ArrayList if there are errors found, otherwise null
168     */
169    public Messages validateField(BaseForm form, FieldDefinition fieldDefinition) {
170        var validationMessages = new Messages();
171        var splitFieldName = Splitter.on(':').trimResults().omitEmptyStrings().splitToList(fieldDefinition.getFieldName()).toArray(new String[0]);
172        var fieldName = splitFieldName[0];
173        var originalFieldValue = form == null ? null : (String)form.get(fieldName);
174        var fieldValue = originalFieldValue;
175        
176        // Clean the String.
177        fieldValue = StringUtils.getInstance().trimToNull(fieldValue);
178        
179        if(fieldValue == null) {
180            // If its null, and its a required field, then its an error.
181            if(fieldDefinition.getIsRequired()) {
182                validationMessages.add(fieldName, new Message(ERROR_REQUIRED_FIELD));
183            }
184        } else {
185            var fieldType = fieldDefinition.getFieldType();
186            var fieldValidator = fieldTypes.get(fieldType);
187            
188            // Not all fieldTypes have an additional validator class
189            if(fieldValidator != null) {
190                try {
191                    var constructor = fieldValidator.getConstructor(new Class[]{Validator.class, BaseForm.class, Messages.class, String.class, String [].class, FieldDefinition.class});
192                    var baseFieldType = (BaseFieldType)constructor.newInstance(new Object[]{this, form, validationMessages, fieldValue, splitFieldName, fieldDefinition});
193
194                    fieldValue = baseFieldType.validate();
195                } catch (InstantiationException | NoSuchMethodException | IllegalAccessException | InvocationTargetException e) {
196                    //e.printStackTrace();
197                    //e.getCause().printStackTrace();
198                    validationMessages.add(fieldName, new Message(ERROR_INTERNAL_ERROR));
199                    fieldValue = null;
200                    //System.err.println("Validator.validateField: fieldName = " + fieldName + ", Exception");
201                }
202            }
203        }
204        
205        // Put back the value we've cleaned up.
206        if(form != null) {
207            form.set(fieldName, fieldValue);
208        }
209        
210        if(!validationMessages.isEmpty()) {
211            log.info("formName = " + form.getFormName() + ", fieldName = " + fieldName + ", originalFieldValue = \""
212                    + originalFieldValue + "\", fieldValue = \"" + fieldValue + "\", errorList = " + validationMessages);
213        }
214        
215        return validationMessages.isEmpty()? null: validationMessages;
216    }
217    
218    private final static FieldDefinition preferredClobMimeTypeNameFieldDefinition = new FieldDefinition("PreferredClobMimeTypeName", FieldType.MIME_TYPE, false, null, null);
219    
220    public void validatePreferredClobMimeTypeName(Messages formValidationMessages, BaseForm form) {
221        var validationMessages = validateField(form, preferredClobMimeTypeNameFieldDefinition);
222
223        if(validationMessages != null) {
224            formValidationMessages.add(validationMessages);
225        }
226    }
227    
228    public void validateOptions(Messages formValidationMessages, BaseForm form) {
229        var options = form.getOptions();
230
231        if(options != null) {
232            options.forEach((option) -> {
233                var m = Patterns.Option.matcher(option);
234                if (!m.matches()) {
235                    formValidationMessages.add(option, new Message(Validator.ERROR_INVALID_OPTION));
236                }
237            });
238        }
239    }
240    
241    public static String validateLong(String fieldValue) {
242        try {
243            var testLong = Long.valueOf(fieldValue);
244            
245            fieldValue = testLong.toString();
246        } catch (NumberFormatException nfe) {
247            if(fieldValue.equalsIgnoreCase("MAX_VALUE")) {
248                fieldValue = Long.toString(Long.MAX_VALUE);
249            } else {
250                fieldValue = null;
251            }
252        }
253        
254        return fieldValue;
255    }
256    
257    public static String validateUnsignedLong(String unsignedLong) {
258        if(unsignedLong != null) {
259            var m = Patterns.UnsignedNumbers.matcher(unsignedLong);
260
261            if(m.matches()) {
262                unsignedLong = validateLong(unsignedLong);
263            } else {
264                unsignedLong = null;
265            }
266        }
267        
268        return unsignedLong;
269    }
270    
271    public void validateLimits(Messages formValidationMessages, BaseForm form) {
272        var limits = form.getLimits();
273
274        if(limits != null) {
275            limits.keySet().forEach((tableNameSingular) -> {
276                var validLimit = true;
277                var m = Patterns.TableNameSingular.matcher(tableNameSingular);
278                if(m.matches()) {
279                    var limit = limits.get(tableNameSingular);
280
281                    if(limit != null) {
282                        var count = limit.getCount();
283                        var newCount = count == null ? null : validateUnsignedLong(count);
284
285                        if(count == null || newCount != null) {
286                            limit.setCount(newCount);
287                        } else {
288                            validLimit = false;
289                        }
290
291                        if(validLimit) {
292                            var offset = limit.getOffset();
293                            var newOffset = offset == null ? null : validateUnsignedLong(offset);
294
295                            if(offset == null || newOffset != null) {
296                                limit.setOffset(newOffset);
297                            } else {
298                                validLimit = false;
299                            }
300                        }
301                    }
302                } else {
303                    validLimit = false;
304                }
305                if (!validLimit) {
306                    formValidationMessages.add(tableNameSingular, new Message(Validator.ERROR_INVALID_LIMIT));
307                }
308            });
309        }
310    }
311    
312    public ValidationResult validate(BaseForm form, List<FieldDefinition> fieldDefinitions) {
313        var formValidationMessages = new Messages();
314        
315        fieldDefinitions.stream().map((fieldDefinition) ->
316                validateField(form, fieldDefinition)).filter(Objects::nonNull).forEach(formValidationMessages::add);
317        
318        if(form != null) {
319            validatePreferredClobMimeTypeName(formValidationMessages, form);
320            validateOptions(formValidationMessages, form);
321            validateLimits(formValidationMessages, form);
322        }
323
324        var hasErrors = !formValidationMessages.isEmpty();
325
326        return new ValidationResult(hasErrors ? formValidationMessages : null);
327    }
328
329}