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}