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}