<!--- *** Refactoring required --->
<!--- v0.3
2017-08-30
Настройки вынесены в таблицу usr_settings в связи с миграцией репозитория пользователей в мастер справочники
Заодно cfqueryparam

модуль должен инкапсулировать работу с настройками
- хранение в сессии и в БД, синхронность с базой
- чтение и модификацию
- очистку(сброс)
При этом модуль ничего не должен знать о структуре самих настроек (фильтрах, сортировках и т.д.) 

Жизненный цикл контейнера settings
1. Живет в сессии
2. Если в сессии нет settings - вычитывается из БД
3. Если и в БД нет - инициализируется и сразу сохраняется в БД (пустой)
4. При модификации синхронизируется с базой (пишет в нее) - нужно либо снаружи вызывать сохранение, либо инкапсулировать запись
(инкапсулировать запись - сильно усложнит код)
5. Для работы выдает КОПИЮ данных, соответственно, при сохранении должен ее синхронизировать??? 
Пока решили, что будем выдавать ссылку на объект в сессии, при этом нужно блок кода синхронизировать CFLOCK
При этом 2 проблемы:
- некрасивый код (лок на сессию руками, связь совершенно неявная)
- избыточная блокировка
- избыточное сохранение (можно было бы проверять на изменение)
чтобы действовать без блокировки, можно экспортировать объект в безопасный scope - переменные или реквест
Альтернативный синтаксис вызова 
<m:settings key="page_name" output="settings"><!--- implicit get --->
	<m:sortparam sort=#settings.sort# fieldcount=9 sortlength=2/><!--- any modification --->
</m:settings><!--- implicit sync --->


Разрешение конфликта версий при обновлении софта
Конфликт версий: код требует параметр, отсутствующий в сохраненной версии. Например, одна версия фильтра сохранила свои настройки, 
а другая, более новая, пытается прочесть несуществующий ключ.
Другой вариант - несуществующее поле (убранное в очередной версии) - этот конфликт проявится только при запросе к БД. 
Варианты решения
- создание параметра с дефолтными значениями при запросе (во все запросы добавить значения по умолчанию). Недостаток - возможно создание помойки
- контроль номера версии. Выглядит сложным в сопровождении
*** Возможно, это была плохая идея - разрешить доступ по длинному ключу. Когда доставали и записывали целиком объект, было меньше возможностей для ошибок
--->

<!---*** обработка вызова get-sync c закрывающим тегом --->
<cfparam name="ATTRIBUTES.action" type="string" default=""/>
<cfparam name="obj" type="struct" default=#structNew()#/>
<cfparam name="doSyncOnClose" type="boolean" default="No"/>

<cfif thisTag.executionMode IS "start">
	<cfif ATTRIBUTES.action IS "">
		<cfset ATTRIBUTES.action="get"/>
		<cfset doSyncOnClose="Yes"/>
	</cfif>		
<cfelse>	
	<cfif doSyncOnClose>
		<cfset ATTRIBUTES.action="sync"/>
	<cfelse>
		<cfexit method="exittag"/>
	</cfif>
</cfif>

<cfset useDB="false"/><!--- --->

<cfswitch expression=#lCase(ATTRIBUTES.action)#>

	<cfcase value="create"> <!--- конструктор --->
		<cflock scope="SESSION" type="EXCLUSIVE" timeout="3">
			<cfset session.settings = retrieveSettings()>
		</cflock>
	</cfcase>
	
	<cfcase value="sync"> 
		<!--- Синхронизировать измененный объект (с сессией и персистенсом) --->
		<cfparam name="ATTRIBUTES.key" type="string">
		<cfif doSyncOnClose>
			<cfif thisTag.executionMode IS "end">
				<cfset obj=StructFind(CALLER, "#ATTRIBUTES.output#")/>
			</cfif>
		<cfelse>			
			<cfparam name="ATTRIBUTES.obj" type="struct">
			<cfset obj=#ATTRIBUTES.obj#/>
		</cfif>
		<!---<cfdump var=#obj#/>--->
		
		<cflock scope="SESSION" type="EXCLUSIVE" timeout="3">			
			<!--- помещаем объект на свое место в структуре --->
			<cfset container=#session.settings#>
			<cfset cnt = listLen("#ATTRIBUTES.key#", ".")>
			<cfloop index="i" from=1 to=#cnt#>
				<cfset item = listGetAt(#ATTRIBUTES.key#, i, ".")>
				<cfif i EQ cnt><!--- last element --->
					<cfset structInsert(#container#, #item#, #obj#, "YES")>
				<cfelse>
					<!--- попытка перебдеть---> 
					<cfif NOT structKeyExists(#container#,#item#)>
						<cfset structInsert(#container#,#item#,#structNew()#)>
					</cfif>
					<cfset container=structFind(container,item)>
				</cfif>
			</cfloop>				
		</cflock>	
		<!--- сохраняем настройки в персистенс --->
		<cfset saveSettings(container)>
	</cfcase>
		
		
	<cfcase value="reset"><!--- *** надо было назвать reset_all или как-то так --->
		<!--- Сбросить все пользовательские настройки --->
		<cfset session.settings=structNew()>
		<cfset saveSettings(session.settings)>
	</cfcase>	
		
	<cfcase value="clear">
		<!--- Очистить секцию пользовательских настроек (sort или filter)--->
		<cfparam name="ATTRIBUTES.key" type="string">		
		<cfset targetFullName = "session.settings.#ATTRIBUTES.key#">
		<cfset objName = listGetAt(ATTRIBUTES.key,1,".")>		
		<cfif isDefined(targetFullName)>
			<cflock scope="SESSION" type="EXCLUSIVE" timeout="3">
				<cfset evaluate("structClear(#targetFullName#)")>
				<cfset obj = evaluate(#targetFullName#)>		
				<cfmodule template="settings.cfm" action="sync" key=#ATTRIBUTES.key# obj=#obj#><!--- синхронизация при очистке используется ---><!--- хорошо ли это? А вообще надо?--->
			</cflock>
		</cfif>
	</cfcase>
		
	<cfcase value="get">
		<!--- 
		Получить объект или секцию
		Обязательный параметр key - имя ключа в структуре, начинающееся с имени объекта, через точку
		В параметре output имя переменной, принимающей результат	
		--->
		<cfparam name="ATTRIBUTES.key" type="string">
		<cfparam name="ATTRIBUTES.output" type="string">
		
		<cfif NOT isDefined("session.settings")><!--- *** container--->
			<cfmodule template="settings.cfm" action="create"/>
		</cfif>
		
		<cfset targetFullName = "session.settings.#ATTRIBUTES.key#">
		<cfset objName = listGetAt(ATTRIBUTES.key, 1, ".")>
		<cfif (NOT structKeyExists(#session.settings#, objName)) 
		OR structIsEmpty(structFind(#session.settings#, objName))>
			<cfmodule template="init_settings.cfm" container=#session.settings# objName=#objName#/>
			<!--- несколько неуклюже, лучше просто возвращать объект и потом класть в сессию --->
		</cfif>
		<cftry>
			<cfset obj=evaluate(targetFullName)>
			<cfcatch type="ANY">
				<cfmodule template="init_settings.cfm" container=#session.settings# objName=#objName#/><!---*** грубо --->
				<cfset obj=evaluate(targetFullName)>
			</cfcatch>
		</cftry>
		<cfset setVariable("CALLER.#ATTRIBUTES.output#", #duplicate(obj)#)>
	</cfcase>	
	<cfdefaultcase>	
		<cfthrow message="Unsupported action = #ATTRIBUTES.action# in settings.cfm">
	</cfdefaultcase>
</cfswitch>	
<!---***debug <cfoutput>#ATTRIBUTES.action# #thisTag.executionMode# #thisTag.hasendtag#</cfoutput>--->



<!--- --------------------------------------------------------------------------------- --->
<!--- private methods --->
<cffunction name="retrieveSettings" access="private">
	<cfif useDB><!--- можно перегрузить методы для разных провайдеров персистенса, но пока смысла в этом не видно, код достаточно примитивен и управляем --->
		<cfquery name="qGet" datasource="#request.DS#">
		select settings from usr where usr_id='#request.usr_id#'
		</cfquery>	
		<cfif len(qGet.settings) GT 0>
			<cftry>				
				<CFWDDX ACTION='wddx2cfml' input=#qGet.settings# output='settings'/>
				<cfreturn #settings#/>
				<cfcatch type="Any">Err: 	
					<cfoutput>#cfcatch.message#</cfoutput>
					<cfabort/>
				</cfcatch>	
			</cftry>
		</cfif>		
	</cfif>
	<cfreturn #structNew()#/>
</cffunction>	

<cffunction name="saveSettings" access="private">
	<cfargument name=settings>
	
	<cfif NOT request.usrAuthenticated()>
			<cfreturn />
	</cfif>
	
	<cfif useDB>		
		<cfquery name="qCheckExistence" datasource="#request.DS#">
			select count(*) as cnt from usr_serrings where usr_id=<cfqueryparam cfsqltype="cf_sql_integer" value='#request.usr_id#'/>  
		</cfquery>
		
		<CFWDDX ACTION='cfml2wddx' input=#session.settings# output='strSettings'/>
		
		<cfif qCheckExistence.cnt EQ 0>
			<cfquery name="qSave" datasource="#request.DS#">
				insert int usr_settings (usr_id, settings) 
				values(<cfqueryparam cfsqltype="cf_sql_integer" value='#request.usr_id#'/>, <cfqueryparam cfsqltype="cf_sql_varchar" value='#strSettings#'/>)
			</cfquery> 
		<cfelse>
			<cfquery name="qSave" datasource="#request.DS#">
				update usr_settings 
				set settings=<cfqueryparam cfsqltype="cf_sql_varchar" value='#strSettings#'/> 
				where usr_id=<cfqueryparam cfsqltype="cf_sql_integer" value='#request.usr_id#'/>
			</cfquery> 
		</cfif>
	<cfelse>
		<!--- do nothing --->
	</cfif>
</cffunction>

