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}