<!--- 
bean (child tag)
msyu@mail.ru
2013-08-29 v0.1
2013-09-04 v0.2 tested on CF8 and Railo
2014-01-20 v0.3 added exception for unsupported parameter type
2014-10-24 v0.5 ATTRIBUTES.field became default for ATTRIBUTES.param
2014-11-18 v0.6 rework
2015-07-21 v0.7 EXITTAG moved to the end of file
2015-07-22 v0.8 Changed protocol for skipInsert, skipUpdate, key, readonly, autoincrement, required attributes, now we can set value at runtime like skipInsert="#boolean#" instead of just including or omitting the attribute
2015-12-02 v0.9 Added preprocessor attribute. Fixed timestamp bug ('01.12.2015' treated as 12 JAN 2015)
2015-12-04 v0.12 for dates, submitted value is now back-overwritten
2015-12-04 v0.13 bug fix
v0.94
2017-03-10 v0.15 - scope resolution order changed to CALLER-FORM-URL
2018-05-31 v0.16 - strip whitespace from numbers
2019-02-09 v0.17 - call preprocessor before checking required field
2022-01-06 v0.18 - added BIT type (for PostgreSQL) - в базе представляется boolean!
2023-04-17 v0.19 - форматирование, добавлен other к idstamp (для postgres)
2023-04-18 v0.20 - добавлен атрибут format, для даты
2024-03-22 v0.21 - добавлены синонимы int,boolean,bool. Перенумерованы версии 91->11
2024-10-16 v0.23 - добавлены атрибуты minlength, maxlength, regex, validate 
--->

<!--- проверка на неподдерживаемые поля --->
<cfloop collection=#ATTRIBUTES# item="a">
	<cfif NOT ListFindNoCase("param,field,type,displayName,default,init,size,scale,skipInsert,skipUpdate,key,readonly,autoincrement,value,null,forNull,eval,required,preprocessor,dtlocale,format,maxlength,minlength,regex,validate", #a#)>
		<cfthrow type="UnsupportedAttribute" message='Attribute #a# is unsupported by bean/param' detail='Specified attribute "#a#" is not supported by custom tag bean/param'/>
	</cfif>
</cfloop>

<cfparam name="ATTRIBUTES.default" default=""/><!--- подставляется при сохранении, если параметр не передавался. Важно для чекбоксов, они не передаются, если не отмечены --->
<cfparam name="ATTRIBUTES.init" default=#ATTRIBUTES.default#/><!--- передается в форму для новой записи (предзаполнение). Для чекбокса может быть init=1, default=0 - отмеченный по умолчанию  --->
<cfparam name="ATTRIBUTES.field"/><!--- имя поля в таблице БД --->
<cfparam name="ATTRIBUTES.param" default="#ATTRIBUTES.field#"/><!--- имя поля в URL/FORM и экспортируемой переменной --->
<cfparam name="ATTRIBUTES.type" default="varchar"/><!--- тип поля в терминах SQL (CF_SQL_*) --->
<cfparam name="ATTRIBUTES.displayName" default=#ATTRIBUTES.param#/><!--- для сообщений об ошибках--->
<cfparam name="ATTRIBUTES.size" default="0"/><!--- for strings, soft limit (causes truncation). 0=unlimited --->
<cfparam name="ATTRIBUTES.scale" default="1"/> <!--- Множитель. Только для чисел, только при записи. --->
<!--- если атрибут записан без значения, он получает значение true! --->
<cfparam name="ATTRIBUTES.skipInsert" type="boolean" default="false"/>
<cfparam name="ATTRIBUTES.skipUpdate" type="boolean" default="false"/>
<cfparam name="ATTRIBUTES.key" type="boolean" default="false"/>
<cfparam name="ATTRIBUTES.readonly" type="boolean" default="false"/>
<cfparam name="ATTRIBUTES.autoincrement" type="boolean" default="false"/><!--- правильнее autogenerated, потому что GUID тоже бывает генеренным --->
<cfparam name="ATTRIBUTES.required" type="boolean" default="false"/>
<cfparam name="ATTRIBUTES.dtlocale" type="string" default="ru"/><!--- datetime locale--->
<cfparam name="ATTRIBUTES.format" type="string" default=""/><!--- datetime format, normally yyyy-MM-dd, CASE SENSITIVE, NOT YYYY-DD-MM!!!--->
<cfparam name="ATTRIBUTES.maxlength" default="0"/><!--- max length hard limit, throws exception unlike ATTRIBUTES.size. 0=unlimited, специально не указываем тип - то есть это по умолчанию строка - чтобы не ломалось от пустой строки--->
<cfparam name="ATTRIBUTES.minlength" default="0"/><!--- min length hard limit, throws exception unlike ATTRIBUTES.size. 0=unlimited, специально не указываем тип - то есть это по умолчанию строка - чтобы не ломалось от пустой строки--->
<!--- <cfparam name="ATTRIBUTES.regex" type="string" default=""/> мы его закомментировали и оставили, чтобы про него не забыть, а дальше мы проверяем его наличие в скоупе атрибутов ---><!--- validation regex --->
<cfparam name="ATTRIBUTES.validate" type="any" default=#function(x){return ""}#/><!--- non-empty return indicates error --->

<!--- ATTRIBUTES.forNull ---><!--- если forNull не задано, параметр считается обязательным ***?--->
<!--- ATTRIBUTES.preprocessor ---><!--- функция --->

<!--- in-out --->
<cfparam name="ATTRIBUTES.null" type="boolean" default="false"/>
<!--- ATTRIBUTES.value - входной-выходной параметр.  --->
<!---/in-out --->

<!--- *** выходные параметры. Странно выглядит. Хоть как-то в имени это отразить, что выходные... --->
<cfparam name="ATTRIBUTES.hasInput" type="boolean" default="false" />
<cfparam name="ATTRIBUTES.submittedValue" type="string" default=""/><!--- необработанное входное значение --->
<cfparam name="ATTRIBUTES.errorState" type="boolean" default="false"/>
<cfparam name="ATTRIBUTES.errorMessage" type="string" default=""/>
<cfparam name="ATTRIBUTES.warningState" type="boolean" default="false"/>
<cfparam name="ATTRIBUTES.warningMessage" type="string" default=""/>

<!--- Тут намешано 2 стиля, и оба не вызывают восторга
Атрибут декларируется с дефолтом (потому что, в отличие от аргумента функции, мы не можем декларировать необязательный атрибут, так как используем cfparam)
Или атрибут не декларируется, а проверяется его наличие в скоупе атрибутов, что не улучшает читаемость кода --->

<cfassociate basetag="cf_bean" datacollection="params" />

<!--- Поле "только для чтения" не обрабатываем никак, если только оно не ключевое. Зачем может понадобиться readonly ключевое поле - непонятно, но вдруг --->
<cfif NOT (ATTRIBUTES.key)>
	<cfif (ATTRIBUTES.readonly)>
		<cfexit method="exittag"/>
	</cfif>
</cfif>
<!--- Если атрибуту явно указан NULL, дальше ничего не делаем --->
<cfif ATTRIBUTES.null>
	<cfset ATTRIBUTES.value="0"/><!--- значение не используется, но переменная должна быть определена --->
	<cfexit method="exittag" />
</cfif>

<!--- принимаем значение из передаваемых данных или подставляем значение по умолчанию --->
<!--- можно было бы попробовать cfparam CALLER.#ATTRIBUTES.param# и обрабатывать исключение, но так обходимся без исключений, хотя и не изящно --->
<cfif StructKeyExists(ATTRIBUTES,"value")>
	<cfset submittedValue=ATTRIBUTES.value/>
	<cfset ATTRIBUTES.hasInput="true"/>
<cfelseif StructKeyExists(CALLER, ATTRIBUTES.param)>
	<cfset submittedValue=structFind(CALLER, ATTRIBUTES.param)/>
	<cfset ATTRIBUTES.hasInput="true"/>
<cfelseif StructKeyExists(FORM, ATTRIBUTES.param)>
	<cfset submittedValue=structFind(FORM, ATTRIBUTES.param)/>
	<cfset ATTRIBUTES.hasInput="true"/>
<cfelseif StructKeyExists(URL, ATTRIBUTES.param)>
	<cfset submittedValue=structFind(URL, ATTRIBUTES.param)/>
	<cfset ATTRIBUTES.hasInput="true"/>

<cfelse>
	<!--- если параметр отсутствует в передаваемых данных --->
	<cfset submittedValue=ATTRIBUTES.default />	
</cfif>	

<cfif !StructKeyExists(ATTRIBUTES,"value")>
	<cfset ATTRIBUTES.value="" /><!---*** dirty fix--->
</cfif>

<cfset ATTRIBUTES.submittedValue=submittedValue />


<cfif StructKeyExists(ATTRIBUTES,"preprocessor")>
	<cfset submittedValue=#ATTRIBUTES.preprocessor(submittedValue)#/>
</cfif>

<!--- check required fields --->	
<cfif ATTRIBUTES.required> 
	<cfif submittedValue EQ "">
		<cfset ATTRIBUTES.errorState = "true"/>
		<cfset ATTRIBUTES.errorMessage = "Не заполнено обязательное поле #ATTRIBUTES.displayName# "/>
		<cfset ATTRIBUTES.value="" />		
		<cfexit method="exittag" />
	</cfif>
</cfif>

<cfif StructKeyExists(ATTRIBUTES, "forNull")>
	<cfif #submittedValue# EQ #ATTRIBUTES.forNull#>
		<cfset ATTRIBUTES.null="true" />
		<cfset ATTRIBUTES.value="" />
		<cfexit method="exittag" />
	</cfif>
</cfif>	

<!--- type validation --->
<cfswitch expression=#ATTRIBUTES.type#>
	<!--- для integer, bigint наличие атрибутa autoincrement подавляет сообщение об ошибке --->
	<!--- *** можно было сделать для всех, но тогда надо обрабатывать исключения --->
	<cfcase value="integer,int,bigint"><!--- *** формат с экспонентой не будет работать --->	
		<cfset s=reReplace(submittedValue, "[[:space:]]", "", "ALL")/>		
		<cfif FindOneOf(",.",s)>
			<cfset s=ListFirst(s,",.")/>
			<cfset ATTRIBUTES.warningState = "true"/>
			<cfset ATTRIBUTES.warningMessage = "Number is truncated #ATTRIBUTES.displayName#: #submittedValue#-&gt;#s#"/>
		</cfif>		

		<!---*** не слишком явно - подавление ошибки для автоинкрементного поля --->
		<cfif (NOT isNumeric(s))>
			<!---<cfif (NOT structKeyExists(ATTRIBUTES, "autoincrement"))>	
			<cfif (NOT structKeyExists(ATTRIBUTES, "autoincrement")) AND (NOT structKeyExists(ATTRIBUTES, "key"))>	--->			
			<cfset ATTRIBUTES.errorState = "true"/>
			<cfset ATTRIBUTES.errorMessage = "Invalid number (#ATTRIBUTES.displayName#: #submittedValue#)"/>			
			<!---<cfthrow type="custom" message="Invalid number" detail="Invalid number (#ATTRIBUTES.displayName#: #submittedValue#)"/>--->
			
			<!--- по-хорошему, выбрасывать исключение нужно только при попытке сохранения
			Но можно и не разбирать в некоторых режимах
			--->
		<cfelse>
			<cfset ATTRIBUTES.value=s*ATTRIBUTES.scale/>
		</cfif>	
	</cfcase>
	
	<cfcase value="guid,uuid,idstamp">
		<!--- guid = PostgreSQL hack --->
		<cfif NOT isValid("guid", submittedValue) AND len(attributes.submittedvalue) GT 0>
			<cfset ATTRIBUTES.errorState = "true" />
			<cfset ATTRIBUTES.errorMessage = "[Error]: Value is not a GUID type." />
			<!--- <cfthrow type="UnsupportedParameterType" 
			message="[Error]: Value is not a GUID type." 
			detail='Value is: #attributes.submittedvalue#'/> --->
		<cfelse>
			<cfset ATTRIBUTES.value = submittedValue />
		</cfif>
	</cfcase>
	
	<cfcase value="numeric,decimal,float,real,double">
		<cfset s=replace(reReplace(submittedValue, "[[:space:]]", "", "ALL"), ",", ".", "ALL")/>
		<cfif NOT isNumeric(s)>
			<cfset ATTRIBUTES.errorState = "true"/>
			<cfset ATTRIBUTES.errorMessage = "Invalid number (#ATTRIBUTES.displayName#: #submittedValue#)"/>	
			<!---<cfthrow type="custom" message="Invalid number" detail="Invalid number (#ATTRIBUTES.displayName#: #submittedValue#)"/>--->
		<cfelse>
			<cfset ATTRIBUTES.value=s*ATTRIBUTES.scale/>
		</cfif>
	</cfcase>	
	
	<cfcase value="bit,boolean,bool"><!--- потому что транслируем примитивно в CF_SQL_BIT--->
		<cfset s=trim(submittedValue)/> <!--- нужно ли это? пробелы действительно дают ошибку --->
		<cfif NOT isBoolean(s)>		
			<cfset ATTRIBUTES.errorState = "true"/>			
			<cfset ATTRIBUTES.errorMessage = "Invalid boolean (#ATTRIBUTES.displayName#: #submittedValue#)"/>	
		<cfelse>
			<!---<cfif s>
				<cfset ATTRIBUTES.value=true/>
			<cfelse>
				<cfset ATTRIBUTES.value=false/>
			</cfif>--->
			<cfset ATTRIBUTES.value=s/>
		</cfif>
	</cfcase>
	
	<cfcase value="date,time,timestamp">
		<cftry>
			<!--- <cfset ATTRIBUTES.value=LSParseDateTime(submittedValue,"#ATTRIBUTES.dtlocale#")/> --->
			<cfset ATTRIBUTES.value=LSParseDateTime(submittedValue,"#ATTRIBUTES.dtlocale#","#ATTRIBUTES.format#")/>
			<cfset ATTRIBUTES.submittedValue=ATTRIBUTES.value/><!--- *** dirty fix--->
			<cfcatch type="ANY">
				<cfset ATTRIBUTES.errorState = "true"/>
				<cfset ATTRIBUTES.errorMessage = "Invalid datetime (#ATTRIBUTES.displayName#: #submittedValue#)"/>	
				<!---<cfthrow type="custom" message="Invalid datetime" detail="Invalid datetime (#ATTRIBUTES.displayName#: #submittedValue#)"/>--->				
			</cfcatch>
		</cftry>
		<!--- пробрасывать сырое значение при ошибке разбора плохо, потому что функция форматирования даты споткнется--->
	</cfcase>
	
	<cfcase value="varchar">
		<cfset submittedValue=trim(submittedValue) />
		<cfparam name="ATTRIBUTES.default" type="string" default=""/>
		<cfset ATTRIBUTES.value=submittedValue/>
		
		<cfif ATTRIBUTES.maxlength GT 0>
			<cfif len(submittedValue) GT ATTRIBUTES.maxlength> 
				<cfset ATTRIBUTES.errorState = "true"/>
				<cfset ATTRIBUTES.errorMessage = "Длина #ATTRIBUTES.displayName# превышает #ATTRIBUTES.maxlength#"/>
				<cfset ATTRIBUTES.value="" />		
				<cfexit method="exittag" />
			</cfif>
		</cfif>		
		
		<cfif ATTRIBUTES.minlength GT 0>
			<cfif len(submittedValue) LT ATTRIBUTES.minlength> 
				<cfset ATTRIBUTES.errorState = "true"/>
				<cfset ATTRIBUTES.errorMessage = "Длина #ATTRIBUTES.displayName# меньше #ATTRIBUTES.minlength#"/>
				<cfset ATTRIBUTES.value="" />		
				<cfexit method="exittag" />
			</cfif>
		</cfif>
		
		<cfif ATTRIBUTES.size GT 0>
			<cfif len(submittedValue) GT ATTRIBUTES.size> 
				<!--- truncate input --->
				<cfset ATTRIBUTES.value=left(submittedValue, ATTRIBUTES.size)/>
				<cfset ATTRIBUTES.warningState = "true"/>
				<cfset ATTRIBUTES.warningMessage = "String is truncated (#ATTRIBUTES.displayName#: #len(submittedValue)#-&gt;#ATTRIBUTES.size#)"/>				
			</cfif>
		</cfif>
	</cfcase>	

	<!---<cfcase value="guid,uuid,uniqueidentifier,idstamp">--->
	
	<cfcase value="other">
		<!--- other = old PostgreSQL hack (incompatibility) --->
		<!--- validation skipped --->
		<cfset ATTRIBUTES.value = submittedValue />
	</cfcase>

	<cfcase value="varbinary">
			<cfset ATTRIBUTES.value=submittedValue/>
	</cfcase>
	
	<cfdefaultcase>
		<cfset ATTRIBUTES.value=""/>
		<cfthrow type="UnsupportedParameterType" message="Specified parameter type is unsupported by current version of bean/param" detail='Parameter type "#ATTRIBUTES.type#" is not supported by custom tags bean/param'/>
	</cfdefaultcase>
	
		<!---<cfcase value="hex">
		<cfset s=submittedValue/>
		<cfif reFindNoCase("[^0-9A-F]", s)>
			<cfset ATTRIBUTES.errorState = "true"/>
			<cfset ATTRIBUTES.errorMessage = "Invalid HEXADECIMAL number (#ATTRIBUTES.displayName#: #submittedValue#)"/>	
		<cfelse>
			<cftry>
				<cfset ATTRIBUTES.value=inputBaseN(s, 16)/>
				<cfcatch type="any">
					<cfset ATTRIBUTES.errorState = "true"/>
					<cfset ATTRIBUTES.errorMessage = "Error converting from HEXADECIMAL format (#ATTRIBUTES.displayName#: #submittedValue#)"/>	
				</cfcatch>
			</cftry>
		</cfif>
	</cfcase>--->
</cfswitch>

<!--- regular expression validation --->
<cfif StructKeyExists(ATTRIBUTES,"regex")>
	<cfif reFind(ATTRIBUTES.regex,submittedValue) EQ 0>
		<cfset ATTRIBUTES.errorState = "true"/>
		<cfset ATTRIBUTES.errorMessage = 'Pattern "#ATTRIBUTES.regex#" does not match "#submittedValue#"'/>
		<cfset ATTRIBUTES.value="" />		
		<cfexit method="exittag" />
	</cfif>
</cfif>
<!--- custom validation --->
<cfif StructKeyExists(ATTRIBUTES,"validate")>
	<cfset validationMessage=ATTRIBUTES.validate(submittedValue)/>
	<cfif len(validationMessage)>
		<cfset ATTRIBUTES.errorState = "true"/>
		<cfset ATTRIBUTES.errorMessage = validationMessage/>
		<cfset ATTRIBUTES.value="" />		
		<cfexit method="exittag" />
	</cfif>
</cfif>

<cfexit method="EXITTAG" />

<!--- это искусственный прием 
<cfif ATTRIBUTES.errorState AND NOT structKeyExists(ATTRIBUTES, "autoincrement") AND NOT structKeyExists(ATTRIBUTES, "key")>
	<cfset getBaseTagData("cf_bean").validationOk="false"/>
</cfif>	
--->
<!--- 
CF_SQL_BIGINT 
CF_SQL_BIT 
CF_SQL_CHAR 
CF_SQL_BLOB 
CF_SQL_CLOB 
CF_SQL_DATE 
CF_SQL_DECIMAL 
CF_SQL_DOUBLE 
CF_SQL_FLOAT 
CF_SQL_IDSTAMP 
CF_SQL_INTEGER 
CF_SQL_LONGVARCHAR 
CF_SQL_MONEY 
CF_SQL_MONEY4 
CF_SQL_NUMERIC 
CF_SQL_REAL 
CF_SQL_REFCURSOR 
CF_SQL_SMALLINT 
CF_SQL_TIME 
CF_SQL_TIMESTAMP 
CF_SQL_TINYINT 
CF_SQL_VARCHAR
*** CF_SQL_OTHER - используется с Postgre для GUID и jsonb
--->
<!--- при ошибочном вводе, например, некорректном формате, возможно два пути
- сохранить, подставив дефолт (в том числе нулл). Можно генерировать ворнинг, цепляя его прямо к полю. В этом случае надо требовать корректные дефолты
- можно ввести раздельные понятия дефолт и инит. 
Разница между дефолтом и инитом. При инициализации новой

Как дефолт, так и инит имеют целевой тип (число, дата...)
Дефолт и инит используются для начального заполнения полей нового объекта. Для строки дефолт-дефолт - пустая, для остальных нулл 
Рассмотреть обязательные и необязательные параметры?
Можно 
- выгрузить некорректный набор данных для редактирования. При втором варианте нужно все выгружать в стринги? Тогда как будут работать функции форматирования в теле страницы, ожидающе не строку? Скажем, дату? По ближайшей догадке? Пустую строку?
Попытка вставить нулл в колонку NOT NULL должна пресекаться заранее (до ошибки БД)
Нулловые строки можно не рассматривать, приводя к пустой строке. 

Тэг принимает параметр от формы, парсит и отдает в атрибуте value. Если не получает, не отдает, можно это специально указывать флагом. Можно ему лишнего не делегировать.
Остальное оставить вызывающему тегу?
 --->
	

