9388b961caa898330c0069c9a78b291e7e3b1718
[tine20] / tine20 / Tinebase / ModelConfiguration.php
1 <?php
2 /**
3  * Tine 2.0
4  *
5  * @package     Tinebase
6  * @subpackage  Configuration
7  * @license     http://www.gnu.org/licenses/agpl.html AGPL Version 3
8  * @copyright   Copyright (c) 2013-2016 Metaways Infosystems GmbH (http://www.metaways.de)
9  * @author      Alexander Stintzing <a.stintzing@metaways.de>
10  */
11
12 /**
13  * Tinebase_ModelConfiguration
14  *
15  * @package     Tinebase
16  * @subpackage  Configuration
17  *
18
19  * these properties are availbale throug __get which prefixes them with _
20  *
21  * @property array      $availableApplications this holds (caches) the availability info of applications globally
22  * @property string     $idProperty the id property
23  * @property array      $table table definition
24  * @property string     $version model version
25  * @property string     $identifier legacy
26  * @property string     $recordName Human readable name of the record
27  * @property string     $recordsName Human readable name of multiple records
28  * @property string     $containerProperty The property of the container, if any
29  * @property boolean    $containerUsesFilter set this to false, if no filter and grid column should be created - default is true
30  * @property boolean    $hasPersonalContainer set this to false, if personal containers should be ommited - default is true
31  * @property string     $titleProperty The property of the title, if any - if an array is given, the second item is the array of arguments for vsprintf, the first the format string
32  * @property boolean    $exposeJsonApi If this is true, the json api (smd) is generated automatically
33  * @property string     $containerName Human readable name of the container
34  * @property string     $containersName Human readable name of multiple containers
35  * @property boolean    $hasRelations If this is true, the record has relations
36  * @property boolean    $hasCustomFields If this is true, the record has customfields
37  * @property boolean    $hasNotes If this is true, the record has notes
38  * @property boolean    $hasTags If this is true, the record has tags
39  * @property boolean    $hasAttachments If this is true, the record has file attachments
40  * @property boolean    $modlogActive If this is true, a modlog will be created
41  * @property boolean    $multipleEdit If this is true, multiple edit of records of this model is possible
42  * @property string     $multipleEditRequiredRight If multiple edit requires a special right, it's defined here
43  * @property boolean    $splitButton if this is set to true, this model will be added to the global add splitbutton
44  * @property string     $moduleGroup Group name of this model (will create a parent node in the modulepanel with this name)
45  * @property string     $defaultFilter Set the default Filter (defaults to query)
46  * @property array      $defaultSortInfo Set the default sort info for the gridpanel (Tine.widgets.grid.GridPanel.defaultSortInfo)
47  * @property string     $requiredRight Defines the right to see this model
48  * @property boolean    $singularContainerMode no containers
49  * @property array      $fields Holds the field definitions in an associative array
50  * @property boolean    $resolveVFGlobally if this is set to true, all virtual fields get resolved by the record controller method "resolveVirtualFields"
51  * @property array      $recordsFields holds all field definitions of type records
52  * @property array      $recordFields holds all field definitions of type record (foreignId fields)
53  * @property boolean    $resolveRelated if this is set to true, related data will be fetched on fetching dependent records by frontend json
54  * @property array      $virtualFields holds virtual field definitions used for non-persistent fields getting calculated on each call of the record
55  * @property array      $fieldGroups maps fieldgroup keys to their names
56  * @property array      $fieldGroupRights here you can define one right (Tinebase_Acl_Rights_Abstract) for each field
57  * @property array      $fieldGroupFeDefaults every field group will be nested into a fieldset, here you can define the defaults (Ext.Container.defaults)
58  * @property boolean    $createModule
59  * @property boolean    $useGroups If any field has a group, this will be set to true (autoset by the constructor)
60  * @property string     $appName the application this configuration belongs to (if the class has the name "Calendar_Model_Event", this will be resolved to "Calendar")
61  * @property string     $application legacy
62  * @property string     $applicationName legacy
63  * @property string     $modelName the name of the model (if the class has the name "Calendar_Model_Event", this will be resolved to "Event")
64  * @property array      $fieldKeys holds the keys of all fields
65  * @property array      $timeFields holds the time fields
66  * @property array      $modlogOmitFields holds the fields which will be omitted in the modlog
67  * @property array      $readOnlyFields these fields will just be readOnly
68  * @property array      $datetimeFields holds the datetime fields
69  * @property array      $dateFields holds the date fields (maybe we use Tinebase_Date sometimes)
70  * @property array      $alarmDateTimeField holds the alarm datetime fields
71  * @property array      $filterModel The calculated filters for this model (auto set)
72  * @property array      $validators holds the validators for the model (auto set)
73  * @property array      $ownValidators holds validators which will be instanciated on model construction
74  * @property boolean    $isDependent if a record is dependent to another, this is true
75  * @property array      $filters input filters (will be set by field configuration)
76  * @property array      $converters converters (will be set by field configuration)
77  * @property array      $defaultData Holds the default Data for the model (autoset from field config)
78  * @property array      $autoincrementFields holds the fields of type autoincrement (will be auto set by field configuration)
79  * @property array      $duplicateCheckFields holds the fields / groups to check for duplicates (will be auto set by field configuration)
80  * @property array      $filterProperties properties to collect for the filters (_appName and _modelName are set in the filter)
81  * @property array      $modelProperties properties to collect for the model
82  * @property array      $frontendProperties properties to collect for the frontend
83  * @property string     $group the module group (will be on the same leaf of the content type tree panel)
84  * @property array      $modelConfiguration the backend properties holding the collected properties
85  * @property array      $frontendConfiguration holds the collected values for the frontendconfig (autoset on first call of getFrontendConfiguration)
86  * @property array      $filterConfiguration the backend properties holding the collected properties
87  * @property array      $attributeConfig
88  * @property array      $filterModelMapping This defines the filters use for all known types
89  * @property array      $inputFilterDefaultMapping This maps field types to own validators, which will be instanciated in the constructor.
90  * @property array      $validatorMapping This maps field types to their default validators, just zendfw validators can be used here.
91  * @property array      $converterDefaultMapping This maps field types to their default converter
92  * @property array      $copyOmitFields Collection of copy omit properties for frontend
93  */
94
95 class Tinebase_ModelConfiguration {
96
97     /**
98      * this holds (caches) the availability info of applications globally
99      * 
100      * @var array
101      */
102     static protected $_availableApplications = array('Tinebase' => TRUE);
103
104     /**
105      * the id property
106      *
107      * @var string
108      */
109     protected $_idProperty = 'id';
110
111     /**
112      * table definition
113      *
114      * @var array
115      */
116     protected $_table = array();
117
118     /**
119      * model version
120      *
121      * @var integer
122      */
123     protected $_version = null;
124     
125     // legacy
126     protected $_identifier;
127
128     /**
129      * Human readable name of the record
130      * add plural translation information in comments like:
131      * // ngettext('Record Name', 'Records Name', 1);
132      *
133      * @var string
134      */
135     protected $_recordName = NULL;
136
137     /**
138      * Human readable name of multiple records
139      * add plural translation information in comments like:
140      * // ngettext('Record Name', 'Records Name', 2);
141      *
142      * @var string
143      */
144     protected $_recordsName = NULL;
145
146     /**
147      * The property of the container, if any
148      *
149      * @var string
150      */
151     protected $_containerProperty = NULL;
152     
153     /**
154      * set this to false, if no filter and grid column should be created
155      * 
156      * @var boolean
157      */
158     protected $_containerUsesFilter = TRUE;
159
160     /**
161      * set this to false, if personal containers should be ommited
162      *
163      * @var boolean
164      */
165     protected $_hasPersonalContainer = TRUE;
166
167     /**
168      * The property of the title, if any
169      *
170      * if an array is given, the second item is the array of arguments for vsprintf, the first the format string
171      *
172      * @var string/array
173      */
174     protected $_titleProperty = 'title';
175
176     /**
177      * If this is true, the json api (smd) is generated automatically
178      *
179      * @var boolean
180      */
181     protected $_exposeJsonApi = NULL;
182
183     /**
184      * If this is true, the http api (smd) is generated automatically
185      *
186      * @var boolean
187      */
188     protected $_exposeHttpApi = NULL;
189
190     /**
191      * Human readable name of the container
192      * add plural translation information in comments like:
193      * // ngettext('Record Name', 'Records Name', 2);
194      *
195      * @var string
196      */
197     protected $_containerName = NULL;
198
199     /**
200      * Human readable name of multiple containers
201      * add plural translation information in comments like:
202      * // ngettext('Record Name', 'Records Name', 2);
203      *
204      * @var string
205      */
206     protected $_containersName = NULL;
207
208     /**
209      * If this is true, the record has relations
210      *
211      * @var boolean
212      */
213     protected $_hasRelations = NULL;
214
215     /**
216      * If this is true, the record has customfields
217      *
218      * @var boolean
219      */
220     protected $_hasCustomFields = NULL;
221
222     /**
223      * If this is true, the record has notes
224      *
225      * @var boolean
226      */
227     protected $_hasNotes = NULL;
228
229     /**
230      * If this is true, the record has tags
231      *
232      * @var boolean
233      */
234     protected $_hasTags = NULL;
235
236     /**
237      * If this is true, the record has file attachments
238      *
239      * @var boolean
240      */
241     protected $_hasAttachments = NULL;
242
243     /**
244      * If this is true, the record has extended properties
245      *
246      * @var boolean
247      */
248     protected $_hasXProps = NULL;
249     
250     /**
251      * If this is true, a modlog will be created
252      *
253      * @var boolean
254      */
255     protected $_modlogActive = NULL;
256
257     /**
258      * If this is true, multiple edit of records of this model is possible.
259      *
260      * @todo add this to a "frontend configuration / uiConfig"
261      *
262      * @var boolean
263      */
264     protected $_multipleEdit = NULL;
265
266     /**
267      * If multiple edit requires a special right, it's defined here
268      *
269      * @var string
270      */
271     protected $_multipleEditRequiredRight = NULL;
272
273     /**
274      * if this is set to true, this model will be added to the global add splitbutton
275      *
276      * @todo add this to a "frontend configuration / uiConfig"
277      * 
278      * @var boolen
279      */
280     protected $_splitButton = FALSE;
281     
282     /**
283      * Group name of this model (will create a parent node in the modulepanel with this name)
284      * add translation information in comments like: // _('Group')
285      *
286      * @var string
287      */
288     protected $_moduleGroup = NULL;
289
290     /**
291      * Set the default Filter (defaults to query)
292      *
293      * @var string
294      */
295     protected $_defaultFilter = 'query';
296
297     /**
298      * Set the default sort info for the gridpanel (Tine.widgets.grid.GridPanel.defaultSortInfo)
299      * set as array('field' => 'title', 'direction' => 'DESC')
300      *
301      * @var array
302      */
303     protected $_defaultSortInfo = NULL;
304
305     /**
306      * Defines the right to see this model
307      *
308      * @var string
309      */
310     protected $_requiredRight = NULL;
311
312     /**
313      * no containers
314      * 
315      * @var boolean
316      */
317     protected $_singularContainerMode = NULL;
318
319     /**
320      * Holds the field definitions in an associative array where the key
321      * corresponds to the db-table name. Possible definitions and their defaults:
322      *
323      * !! Get sure to have at least one default value set and added one field to the query filter !!
324      *
325      * - validators: Use Zend Input Filters to validate the values.
326      *       @type: Array, @default: array(Zend_Filter_Input::ALLOW_EMPTY => true)
327      *
328      * - label: The human readable label of the field. If this is set to null, this won't be shown in the auto FE Grid or EditDialog.
329      *       Add translation information in comments like: // _('Title')
330      *       @type: String, @default: NULL
331      *
332      * - default: The default value of the field.
333      *       Add translation information in comments like: // _('New Car')
334      *       @type: as defined (see DEFAULT MAPPING), @default: NULL
335      *
336      * - duplicateCheckGroup: All Fields having the same group will be combined for duplicate
337      *       check. If no group is given, no duplicate check will be done.
338      *       @type: String, @default: NULL
339      *
340      * - type: The type of the Value
341      *       @type: String, @default: "string"
342      *
343      * - specialType: Defines the type closer
344      *       @type: String, @default: NULL
345      *
346      * - filterDefinition: Overwrites the default filter used for this field
347      *       Definition knows all from Tinebase_Model_Filter_FilterGroup._filterModel
348      *       @type: Array, @default: array('filter' => 'Tinebase_Model_Filter_Text') or the default used for this type (see DEFAULT MAPPING)
349      * 
350      * - inputFilters: zend input filters to use for this field
351      *       @type: Array, use array(<InPutFilterClassName> => <constructorData>, ...)
352      * 
353      * - queryFilter: If this is set to true, this field will be used by the "query" filter
354      *       @type: Boolean, @default: NULL
355      *
356      * - duplicateOmit: Will neither be shown nor handled on duplicate resolving
357      *       @type: Boolean, @default: NULL
358      *
359      * - copyOmit: If this is set to true, the field won't be used on copy the record
360      *       @type: Boolean, @default: NULL
361      *
362      * - readOnly: If this is set to true, the field can't be updated and will be shown as readOnly in the frontend
363      *       @type: Boolean, @default: NULL
364      *
365      * - disabled: If this is set to true, the field can't be updated and will be shown as readOnly in the frontend
366      *       @type: Boolean, @default: NULL
367      *
368      * - group: Add this field to a group. Each group will be shown as a separate FieldSet of the
369      *       EditDialog and group in the DuplicateResolveGridPanel. If any field of this model
370      *       has a group set, FieldSets will be created and fields without a group set will be
371      *       added to a group with the same name as the RecordName.
372      *       Add translation information in comments like: // _('Group')
373      *       @type: String, @default: NULL
374      *
375      * - dateFormat: If type is a date, the format can be overwritten here
376      *       @type: String, @default: NULL or the default used for this type (see DEFAULT MAPPING)
377      *
378      * - shy: If this is set to true, the row for this field won't be shown in the grid, but can be activated
379      * 
380      * - sortable: If this is set to false, no sort by this field is possible in the gridpanel, defaults to true
381      * 
382      *   // TODO: generalize, currently only in ContractGridPanel, take it from there:
383      * - showInDetailsPanel: auto show in details panel if any is defined in the js gridpanel class
384      * 
385      * - useGlobalTranslation: if set, the global translation is used
386      * 
387      * DEFAULT MAPPING:
388      *
389      * Field-Type  specialType   Human-Type          SQL-Type JS-Type                       PHP-Type          PHP-Filter                  dateFormat    JS-FilterType
390      *
391      * date                      Date                datetime date                          Tinebase_DateTime Tinebase_Model_Filter_Date  ISO8601Short
392      * datetime                  Date with time      datetime date                          Tinebase_DateTime Tinebase_Model_Filter_Date  ISO8601Long
393      * time                      Time                datetime date                          Tinebase_DateTime Tinebase_Model_Filter_Date  ISO8601Time
394      * string                    Text                varchar  string                        string            Tinebase_Model_Filter_Text
395      * text                      Text with lnbr.     text     string                        string            Tinebase_Model_Filter_Text
396      * fulltext                  Text with lnbr.     text     string                        string            Tinebase_Model_Filter_FullText
397      * boolean                   Boolean             boolean  bool                          bool              Tinebase_Model_Filter_Bool
398      * integer                   Integer             integer  integer                       int               Tinebase_Model_Filter_Int                 number
399      * integer     percent       Integer             integer  integer                       int               Tinebase_Model_Filter_Int                 extuxnumberfield
400      * integer     bytes         Bytes               integer  integer                       int               Tinebase_Model_Filter_Int
401      * integer     seconds       Seconds             integer  integer                       int               Tinebase_Model_Filter_Int
402      * integer     minutes       Minutes             integer  integer                       int               Tinebase_Model_Filter_Int
403      * float                     Float               float    float                         float             Tinebase_Model_Filter_Int                 extuxnumberfield
404      * float       percent       Float               float    float                         float             Tinebase_Model_Filter_Int
405      * float       money         value and currency  float    float                         int               Tinebase_Model_Filter_Int
406      * json                      Json String         text     string                        array             Tinebase_Model_Filter_Text
407      * container                 Container           string   Tine.Tinebase.Model.Container Tinebase_Model_Container                                    tine.widget.container.filtermodel
408      * tag tinebase.tag
409      * user                      User                string                                 Tinebase_Model_Filter_User
410      * virtual:
411      * 
412      * Field Type "virtual" has a config property which holds the field configuration.
413      * An additional property is "function". If this property is set, the given function
414      * will be called to resolve the field in Tinebase_Convert_Json.
415      * If an array with two values is given, the first value will be handled as a class,
416      * the second one would be handled as a statically callable method.
417      * if the array is an associative one with one key and one value, 
418      * the key will be used for the classname of a singleton (callable by getInstance),
419      * the value will be used as method name.
420      * 
421      * * record                  1:1 - Relation      text     Tine.<APP>.Model.<MODEL>      Tinebase_Record_Abstract  Tinebase_Model_Filter_ForeignId   Tine.widgets.grid.ForeignRecordFilter
422      * * records                 1:n - Relation      -        Array of Record.data Objects  Tinebase_Record_RecordSet -                                 -
423      * * relation                m:m - Relation      -        Tinebase.Model.Relation       Tinebase_Model_Relation   Tinebase_Model_Filter_Relation
424      * * keyfield                String              string   <as defined>                  string            Tinebase_Model_Filter_Text
425      *
426      * * Accepts additional parameter: 'config' => array with these keys:
427      *     - @string appName    (the name of the application of the referenced record/s)
428      *     - @string modelName  (the name of the model of the referenced record/s)
429      *     - @boolean doNotCheckModuleRight (set to true to skip the module right check for this field, this allows filters
430      *                           and grid columns to be visible for users that do not have the "view" right for the module
431      *                           for example: timeaccounts in the timetracker-timesheet grid panel)
432      *
433      *   Config for 'record' and 'records' accepts also these keys: (optionally if record class name doesn't fit the convention, will be autogenerated, if not set)
434      *     - @string recordClassName 
435      *     - @string controllerClassName
436      *     
437      *   Config for 'records' accepts also these keys:
438      *     - @string refIdField (the field of the foreign record referencing the idProperty of the own record)
439      *     - @array  paging     (accepts the parameters as Tinebase_Model_Pagination does)
440      *     - @string filterClassName
441      *     - @array  addFilters define them as array like Tinebase_Model_Filter_FilterGroup
442      * 
443      * record accepts keys additionally
444      *     - @string isParent   set this to true if the field is the parent property of an dependent record. This field will be hidden in an edit dialog nested grid
445      *     
446      * records accepts keys additionally
447      *     - @string omitOnSearch set this to FALSE, if the field should be resolved on json-search (defaults to TRUE)
448      *     
449      * <code>
450      *
451      * array(
452      *     'title' => array(
453      *         'validators' => array(Zend_Filter_Input::ALLOW_EMPTY => true),
454      *         'label' => NULL,
455      *         'duplicateCheckGroup' => 'number'
456      *     ),
457      *     'number' => ...
458      * )
459      *
460      * </code>
461      *
462      * @var array
463      */
464     protected $_fields = array();
465     
466     /**
467      * if this is set to true, all virtual fields get resolved by the record controller method "resolveVirtualFields"
468      *
469      * @var boolean
470      */
471     protected $_resolveVFGlobally = FALSE;
472     
473     /**
474      * holds all field definitions of type records
475      *
476      * @var array
477     */
478     protected $_recordsFields = NULL;
479
480     /**
481      * holds all field definitions of type record (foreignId fields)
482      *
483      * @var array
484      */
485     protected $_recordFields  = NULL;
486
487     /**
488      * if this is set to true, related data will be fetched on fetching dependent records by frontend json
489      * look at: Tinebase_Convert_Json._resolveMultipleRecordFields
490      * 
491      * @var boolean
492      */
493     protected $_resolveRelated = FALSE;
494     
495     /**
496      * holds virtual field definitions used for non-persistent fields getting calculated on each call of the record
497      * no backend property will be build, no filters etc. will exist. they must be filled in frontend json
498      * 
499      * @var array
500      */
501     protected $_virtualFields = [];
502     
503     /**
504      * maps fieldgroup keys to their names
505      * Add translation information in comments like: // _('Banking Information')
506      * 
507      * array(
508      *     'banking' => 'Banking Information',    // _('Banking Information')
509      *     'private' => 'Private Information',    // _('Private Information')
510      *     )
511      * 
512      * @var array
513      */
514     protected $_fieldGroups = NULL;
515     
516     /**
517      * here you can define one right (Tinebase_Acl_Rights_Abstract) for each field
518      * group ('group'-property of a field definition of this._fields), the user must
519      * have to see/edit this group, otherwise the fields of the edit dialog will be disabled/readOnly
520      *
521      * array(
522      *     'private' => array(
523      *         'see'  => HumanResources_Acl_Rights::SEE_PRIVATE,
524      *         'edit' => HumanResources_Acl_Rights::EDIT_PRIVATE,
525      *     ),
526      *     'banking' => array(
527      *         'see'  => HumanResources_Acl_Rights::SEE_BANKING,
528      *         'edit' => HumanResources_Acl_Rights::EDIT_BANKING,
529      *     )
530      * );
531      *
532      * @var array
533      */
534     protected $_fieldGroupRights = array();
535
536     /**
537      * every field group will be nested into a fieldset, here you can define the defaults (Ext.Container.defaults)
538      *
539      * @var array
540     */
541     protected $_fieldGroupFeDefaults = array();
542
543     protected $_createModule = FALSE;
544     
545     /*
546      * auto set by the constructor
547     */
548
549     /**
550      * If any field has a group, this will be set to true (autoset by the constructor)
551      *
552      * @var boolean
553     */
554     protected $_useGroups = FALSE;
555
556     /**
557      * the application this configuration belongs to (if the class has the name "Calendar_Model_Event", this will be resolved to "Calendar")
558      *
559      * @var string
560      */
561     protected $_appName = NULL;    // this should be used everytime, everywhere
562     // legacy
563     protected $_application = NULL;
564     protected $_applicationName = NULL;
565     /**
566      * the name of the model (if the class has the name "Calendar_Model_Event", this will be resolved to "Event")
567      *
568      * @var string
569      */
570     protected $_modelName = NULL;
571
572     /**
573      * holds the keys of all fields
574      *
575      * @var array
576      */
577     protected $_fieldKeys = array();
578
579     /**
580      * holds the time fields
581      *
582      * @var array
583     */
584     protected $_timeFields = array();
585
586     /**
587      * holds the fields which will be omitted in the modlog
588      *
589      * @var array
590     */
591     protected $_modlogOmitFields = array();
592
593     /**
594      * these fields will just be readOnly
595      *
596      * @var array
597     */
598     protected $_readOnlyFields = array();
599
600     /**
601      * holds the datetime fields
602      *
603      * @var array
604     */
605     protected $_datetimeFields = array();
606
607     /**
608      * holds the date fields (maybe we use Tinebase_Date sometimes)
609      * 
610      * @var array
611      */
612     protected $_dateFields = array();
613     
614     /**
615      * holds the alarm datetime fields
616      *
617      * @var array
618     */
619     protected $_alarmDateTimeField = array();
620
621     /**
622      * The calculated filters for this model (auto set)
623      *
624      * @var array
625     */
626     protected $_filterModel = array();
627
628     /**
629      * holds the validators for the model (auto set)
630     */
631     protected $_validators = array();
632
633     /**
634      * holds validators which will be instanciated on model construction
635      *
636      * @var array
637     */
638     protected $_ownValidators = array();
639     
640     /**
641      * if a record is dependent to another, this is true
642      * 
643      * @var boolean
644      */
645     protected $_isDependent = FALSE;
646     
647     /**
648      * input filters (will be set by field configuration)
649      * 
650      * @var array
651      */
652     protected $_filters;
653
654     /**
655      * converters (will be set by field configuration)
656      *
657      * @var array
658      */
659     protected $_converters = array();
660     
661     /**
662      * Holds the default Data for the model (autoset from field config)
663      *
664      * @var array
665     */
666     protected $_defaultData = array();
667
668     /**
669      * holds the fields of type autoincrement (will be auto set by field configuration)
670      */
671     protected $_autoincrementFields = array();
672
673     /**
674      * holds the fields / groups to check for duplicates (will be auto set by field configuration)
675     */
676     protected $_duplicateCheckFields = NULL;
677
678     /**
679      * properties to collect for the filters (_appName and _modelName are set in the filter)
680      *
681      * @var array
682      */
683     protected $_filterProperties = array('_filterModel', '_defaultFilter', '_modelName', '_applicationName');
684
685     /**
686      * properties to collect for the model
687      *
688      * @var array
689     */
690     protected $_modelProperties = array(
691         '_identifier', '_timeFields', '_dateFields', '_datetimeFields', '_alarmDateTimeField', '_validators', '_modlogOmitFields',
692         '_application', '_readOnlyFields', '_filters'
693     );
694
695     /**
696      * properties to collect for the frontend
697      *
698      * @var array
699     */
700     protected $_frontendProperties = array(
701         'containerProperty', 'containersName', 'containerName', 'defaultSortInfo', 'fieldKeys', 'filterModel',
702         'defaultFilter', 'requiredRight', 'singularContainerMode', 'fields', 'defaultData', 'titleProperty',
703         'useGroups', 'fieldGroupFeDefaults', 'fieldGroupRights', 'multipleEdit', 'multipleEditRequiredRight',
704         'copyEditAction', 'copyOmitFields', 'recordName', 'recordsName', 'appName', 'modelName', 'createModule',
705         'isDependent', 'hasCustomFields', 'modlogActive', 'hasAttachments', 'idProperty', 'splitButton',
706         'attributeConfig', 'hasPersonalContainer', 'import', 'export', 'virtualFields', 'group',
707     );
708
709     /**
710      * the module group (will be on the same leaf of the content type tree panel
711      * 
712      * @var string
713      */
714     protected $_group = NULL;
715     
716     /**
717      * the backend properties holding the collected properties
718      *
719      * @var array
720     */
721     protected $_modelConfiguration = NULL;
722
723     /**
724      * holds the collected values for the frontendconfig (autoset on first call of getFrontendConfiguration)
725      * 
726      * @var array
727      */
728     protected $_frontendConfiguration = NULL;
729     
730     /**
731      * the backend properties holding the collected properties
732      *
733      * @var array
734      */
735     protected $_filterConfiguration = NULL;
736
737     /**
738      *
739      * @var array
740      */
741     protected $_attributeConfig = NULL;
742
743     /*
744      * mappings
745      */
746
747     /**
748      * This defines the filters use for all known types
749      * @var array
750      *
751      * NOTE: "records" type has no automatic filter definition/mapping!
752      * TODO generalize this for "records" type (see Sales_Model_Filter_ContractProductAggregateFilter)
753      */
754     protected $_filterModelMapping = array(
755         'date'     => 'Tinebase_Model_Filter_Date',
756         'datetime' => 'Tinebase_Model_Filter_DateTime',
757         'time'     => 'Tinebase_Model_Filter_Date',
758         'string'   => 'Tinebase_Model_Filter_Text',
759         'text'     => 'Tinebase_Model_Filter_Text',
760         'fulltext' => 'Tinebase_Model_Filter_FullText',
761         'json'     => 'Tinebase_Model_Filter_Text',
762         'boolean'  => 'Tinebase_Model_Filter_Bool',
763         'integer'  => 'Tinebase_Model_Filter_Int',
764         'float'    => 'Tinebase_Model_Filter_Float',
765         'record'   => 'Tinebase_Model_Filter_ForeignId',
766         'relation' => 'Tinebase_Model_Filter_Relation',
767
768         'keyfield'  => 'Tinebase_Model_Filter_Text',
769         'container' => 'Tinebase_Model_Filter_Container',
770         'tag'       => 'Tinebase_Model_Filter_Tag',
771         'user'      => 'Tinebase_Model_Filter_User',
772
773         'numberableStr' => 'Tinebase_Model_Filter_Text',
774         'numberableInt' => 'Tinebase_Model_Filter_Int',
775     );
776
777     /**
778      * This maps field types to own validators, which will be instanciated in the constructor.
779      *
780      * @var array
781     */
782     protected $_inputFilterDefaultMapping = array(
783         'text'     => array('Tinebase_Model_InputFilter_CrlfConvert'),
784         'fulltext' => array('Tinebase_Model_InputFilter_CrlfConvert'),
785     );
786
787     /**
788      * This maps field types to their default validators, just zendfw validators can be used here.
789      * For using own validators, use _ownValidatorMapping instead. If no validator is given,
790      * "array(Zend_Filter_Input::ALLOW_EMPTY => true)" will be used
791      *
792      * @var array
793     */
794     protected $_validatorMapping = array(
795         'record'    => array(Zend_Filter_Input::ALLOW_EMPTY => true, Zend_Filter_Input::DEFAULT_VALUE => NULL),
796         'relation'  => array(Zend_Filter_Input::ALLOW_EMPTY => true, Zend_Filter_Input::DEFAULT_VALUE => NULL),
797     );
798
799     /**
800      * This maps field types to their default converter
801      *
802      * @var array
803      */
804     protected $_converterDefaultMapping = array(
805         'json'      => array('Tinebase_Model_Converter_Json'),
806     );
807
808     /**
809      * If this is true, copy of records of this model is possible.
810      *
811      * @todo add this to a "frontend configuration / uiConfig"
812      *
813      * @var boolean
814      */
815     protected $_copyEditAction = null;
816
817     /**
818      * Collection of copy omit properties for frontend
819      *
820      * @todo add this to a "frontend configuration / uiConfig"
821      *
822      * @var array
823      */
824     protected $_copyOmitFields = NULL;
825
826     /**
827      * import configuration
828      *
829      * sub keys:
830      *  - defaultImportContainerRegistryKey: contains the registry key for model default comtainer
831      *      for example:
832      *          'defaultImportContainerRegistryKey' => 'defaultInventoryItemContainer',
833      *
834      * @var array
835      */
836     protected $_import = NULL;
837
838     /**
839      * export configuration
840      *
841      * sub keys:
842      *  - supportedFormats: array of supported export formats (in lowercase)
843      *      for example:
844      *          'supportedFormats' => array('csv', 'ods', 'xls'),
845      *
846      * @var array
847      */
848     protected $_export = NULL;
849
850     /**
851      * the constructor (must be called by the singleton pattern)
852      *
853      * @var array $modelClassConfiguration
854      * @throws Tinebase_Exception_Record_DefinitionFailure
855      */
856     public function __construct($modelClassConfiguration)
857     {
858         if (! $modelClassConfiguration) {
859             throw new Tinebase_Exception('The model class configuration must be submitted!');
860         }
861
862         $this->_appName     = $this->_application = $this->_applicationName = $modelClassConfiguration['appName'];
863         
864         // add appName to available applications 
865         self::$_availableApplications[$this->_appName] = TRUE;
866         
867         $this->_modelName   = $modelClassConfiguration['modelName'];
868         $this->_idProperty  = $this->_identifier = isset($modelClassConfiguration['idProperty']) ? $modelClassConfiguration['idProperty'] : 'id';
869
870         $this->_table = isset($modelClassConfiguration['table']) ? $modelClassConfiguration['table'] : $this->_table;
871         $this->_version = isset($modelClassConfiguration['version']) ? $modelClassConfiguration['version'] : $this->_version;
872
873         // some cruid validating
874         foreach ($modelClassConfiguration as $propertyName => $propertyValue) {
875             $this->{'_' . $propertyName} = $propertyValue;
876         }
877         
878         $this->_filters = array();
879         $this->_fields[$this->_idProperty] = array('id' => true, 'label' => NULL, 'validators' => array(Zend_Filter_Input::ALLOW_EMPTY => true), 'length' => 40);
880
881         if ($this->_hasCustomFields) {
882             $this->_fields['customfields'] = array('label' => NULL, 'type' => 'custom', 'validators' => array(Zend_Filter_Input::ALLOW_EMPTY => true, Zend_Filter_Input::DEFAULT_VALUE => NULL));
883         }
884
885         if ($this->_hasRelations) {
886             $this->_fields['relations'] = array('label' => NULL, 'type' => 'relation', 'validators' => array(Zend_Filter_Input::ALLOW_EMPTY => true, Zend_Filter_Input::DEFAULT_VALUE => NULL));
887         }
888
889         if ($this->_containerProperty) {
890             $this->_fields[$this->_containerProperty] = array(
891                 'nullable'         => true,
892                 'unsigned'         => true,
893                 'label'            => $this->_containerUsesFilter ? $this->_containerName : NULL,
894                 'shy'              => true,
895                 'type'             => 'container',
896                 'validators'       => array(Zend_Filter_Input::ALLOW_EMPTY => true),
897                 'filterDefinition' => array(
898                     'filter'  => $this->_filterModelMapping['container'],
899                     'options' => array('applicationName' => $this->_appName)
900                 )
901             );
902         } else {
903             $this->_singularContainerMode = true;
904         }
905
906         // quick hack ('key')
907         if ($this->_hasTags) {
908             $this->_fields['tags'] = array(
909                 'label' => 'Tags',
910                 'sortable' => false,
911                 'type' => 'tag', 
912                 'validators' => array(Zend_Filter_Input::ALLOW_EMPTY => true, Zend_Filter_Input::DEFAULT_VALUE => NULL), 
913                 'useGlobalTranslation' => TRUE,
914                 'filterDefinition' => array(
915                     'key'     => 'tag',
916                     'filter'  => $this->_filterModelMapping['tag'],
917                     'options' => array(
918                            'idProperty' => $this->_getTableName() . '.' . $this->_idProperty,
919                            'applicationName' => $this->_appName
920                     )
921                 )
922             );
923         }
924
925         if ($this->_hasAttachments) {
926             $this->_fields['attachments'] = array(
927                 'label' => NULL,
928                 'type'  => 'attachments'
929             );
930         }
931
932         if ($this->_hasXProps) {
933             $this->_fields['xprops'] = array(
934                 'label' => NULL,
935                 'type'  => 'json',
936                 'validators' => array(Zend_Filter_Input::ALLOW_EMPTY => true, Zend_Filter_Input::DEFAULT_VALUE => array()),
937             );
938         }
939         
940         if ($this->_modlogActive) {
941             // notes are needed if modlog is active
942             $this->_fields['notes']              = array('label' => NULL,                 'type' => 'note',     'validators' => array(Zend_Filter_Input::ALLOW_EMPTY => true, Zend_Filter_Input::DEFAULT_VALUE => NULL), 'useGlobalTranslation' => TRUE);
943             $this->_fields['created_by']         = array('label' => 'Created By',         'type' => 'user',     'validators' => array(Zend_Filter_Input::ALLOW_EMPTY => true), 'shy' => true, 'useGlobalTranslation' => TRUE, 'length' => 40, 'nullable' => true);
944             $this->_fields['creation_time']      = array('label' => 'Creation Time',      'type' => 'datetime', 'validators' => array(Zend_Filter_Input::ALLOW_EMPTY => true), 'shy' => true, 'useGlobalTranslation' => TRUE, 'nullable' => true);
945             $this->_fields['last_modified_by']   = array('label' => 'Last Modified By',   'type' => 'user',     'validators' => array(Zend_Filter_Input::ALLOW_EMPTY => true), 'shy' => true, 'useGlobalTranslation' => TRUE, 'length' => 40, 'nullable' => true);
946             $this->_fields['last_modified_time'] = array('label' => 'Last Modified Time', 'type' => 'datetime', 'validators' => array(Zend_Filter_Input::ALLOW_EMPTY => true), 'shy' => true, 'useGlobalTranslation' => TRUE, 'nullable' => true);
947             $this->_fields['seq']                = array('label' => NULL,                 'type' => 'integer',  'validators' => array(Zend_Filter_Input::ALLOW_EMPTY => true), 'shy' => true, 'useGlobalTranslation' => TRUE, 'default' => 0, 'unsigned' => true);
948             
949             // don't show deleted information
950             $this->_fields['deleted_by']         = array('label' => NULL, 'type' => 'user',     'validators' => array(Zend_Filter_Input::ALLOW_EMPTY => true), 'useGlobalTranslation' => TRUE, 'length' => 40, 'nullable' => true);
951             $this->_fields['deleted_time']       = array('label' => NULL, 'type' => 'datetime', 'validators' => array(Zend_Filter_Input::ALLOW_EMPTY => true), 'useGlobalTranslation' => TRUE, 'nullable' => true);
952             $this->_fields['is_deleted']         = array(
953                 'label'   => NULL,
954                 'type'    => 'integer',
955                 'validators' => array(Zend_Filter_Input::ALLOW_EMPTY => true),
956                 'useGlobalTranslation' => TRUE,
957                 'default' => 0
958             );
959
960         } elseif ($this->_hasNotes) {
961             $this->_fields['notes'] = array('label' => NULL, 'type' => 'note', 'validators' => array(Zend_Filter_Input::ALLOW_EMPTY => true, Zend_Filter_Input::DEFAULT_VALUE => NULL));
962         }
963         
964         // holds the filters used for the query-filter, if any
965         $queryFilters = array();
966         
967         foreach ($this->_fields as $fieldKey => &$fieldDef) {
968             $fieldDef['fieldName'] = $fieldKey;
969
970             if (isset($fieldDef['readOnly'])) {
971                 $this->_readOnlyFields[] = $fieldKey;
972             }
973
974             // set default type to string, if no type is given
975             if (! (isset($fieldDef['type']) || array_key_exists('type', $fieldDef))) {
976                 $fieldDef['type'] = 'string';
977             }
978             
979             // don't handle field if app is not available or feature disabled
980             if ((isset($fieldDef['config']) || array_key_exists('config', $fieldDef))
981                 && ($fieldDef['type'] == 'record' || $fieldDef['type'] == 'records')
982                 && (! $this->_isAvailable($fieldDef['config'])))
983             {
984                 $fieldDef['type'] = 'string';
985                 $fieldDef['label'] = NULL;
986                 continue;
987             }
988             // the property name
989             $fieldDef['key'] = $fieldKey;
990
991             // if any field has a group set, enable grouping globally
992             if (! $this->_useGroups && (isset($fieldDef['group']) || array_key_exists('group', $fieldDef))) {
993                 $this->_useGroups = TRUE;
994             }
995
996             if ($fieldDef['type'] == 'keyfield') {
997                 $fieldDef['length'] = 40;
998             } elseif ($fieldDef['type'] == 'virtual') {
999                 $virtualField = isset($fieldDef['config']) ? $fieldDef['config'] : array();
1000                 $virtualField['key'] = $fieldKey;
1001                 $virtualField['sortable'] = FALSE;
1002                 if ((isset($virtualField['default']))) {
1003                     // @todo: better handling of virtualfields
1004                     $this->_defaultData[$fieldKey] = $virtualField['default'];
1005                 }
1006                 $this->_virtualFields[] = $virtualField;
1007                 $fieldDef['modlogOmit'] = true;
1008
1009             } elseif ($fieldDef['type'] == 'numberableStr' || $fieldDef['type'] == 'numberableInt') {
1010                 $this->_autoincrementFields[] = $fieldDef;
1011             }
1012
1013             if (isset($fieldDef['copyOmit']) && $fieldDef['copyOmit']) {
1014                 if (!is_array($this->_copyOmitFields)) {
1015                     $this->_copyOmitFields = array();
1016                 }
1017                 $this->_copyOmitFields[] = $fieldKey;
1018             }
1019
1020             // set default value
1021             // TODO: implement complex default values
1022             if ((isset($fieldDef['default']))) {
1023 //                 // allows dynamic default values
1024 //                 if (is_array($fieldDef['default'])) {
1025 //                     switch ($fieldDef['type']) {
1026 //                         case 'time':
1027 //                         case 'date':
1028 //                         case 'datetime':
1029 //                         default:
1030 //                             throw new Tinebase_Exception_NotImplemented($_message);
1031 //                     }
1032 //                 } else {
1033                     $this->_defaultData[$fieldKey] = $fieldDef['default'];
1034                     
1035 //                 }
1036             }
1037
1038             // TODO: Split this up in multiple functions
1039             // TODO: Refactor: key 'tag' should be 'tags' in filter definition / quick hack
1040             // also see ticket 8944 (https://forge.tine20.org/mantisbt/view.php?id=8944)
1041             
1042             $this->_setFieldFilterModel($fieldDef, $fieldKey);
1043
1044             if (isset($fieldDef['queryFilter'])) {
1045                 $queryFilters[] = $fieldKey;
1046             }
1047
1048             // set validators
1049             if (isset($fieldDef['validators'])) {
1050                 // use _validators from definition
1051                 $this->_validators[$fieldKey] = $fieldDef['validators'];
1052             } else if ((isset($this->_validatorMapping[$fieldDef['type']]) || array_key_exists($fieldDef['type'], $this->_validatorMapping))) {
1053                 // if no validatorsDefinition is given, try to use the default one
1054                 $fieldDef['validators'] = $this->_validators[$fieldKey] = $this->_validatorMapping[$fieldDef['type']];
1055             } else {
1056                 $fieldDef['validators'] = $this->_validators[$fieldKey] = array(Zend_Filter_Input::ALLOW_EMPTY => true);
1057             }
1058             
1059             // set input filters, append defined if any or use defaults from _inputFilterDefaultMapping 
1060             if (isset($fieldDef['inputFilters'])) {
1061                 foreach ($fieldDef['inputFilters'] as $if => $val) {
1062                     if (is_array($val)) {
1063                         $reflect  = new ReflectionClass($if);
1064                         $this->_filters[$fieldKey][] = $reflect->newInstanceArgs($val);
1065                     } else {
1066                         $this->_filters[$fieldKey][] = $if ? new $if($val) : new $val();
1067                     }
1068                 }
1069             } elseif (isset($this->_inputFilterDefaultMapping[$fieldDef['type']])) {
1070                 foreach ($this->_inputFilterDefaultMapping[$fieldDef['type']] as $if => $val) {
1071                     $this->_filters[$fieldKey][] = $if ? new $if($val) : new $val();
1072                 }
1073             }
1074             
1075             // add field to modlog omit, if configured and modlog is used
1076             if ($this->_modlogActive && isset($fieldDef['modlogOmit'])) {
1077                 $this->_modlogOmitFields[] = $fieldKey;
1078             }
1079
1080             // set converters
1081             if (isset($fieldDef['converters']) && is_array($fieldDef['converters'])) {
1082                 if (count($fieldDef['converters'])) {
1083                     $this->_converters[$fieldKey] = $fieldDef['converters'];
1084                 }
1085             } elseif(isset($this->_converterDefaultMapping[$fieldDef['type']])) {
1086                 $this->_converters[$fieldKey] = $this->_converterDefaultMapping[$fieldDef['type']];
1087             }
1088             
1089             $this->_populateProperties($fieldKey, $fieldDef);
1090             
1091         }
1092         
1093         // set some default filters
1094         if (count($queryFilters)) {
1095             $this->_getQueryFilter($queryFilters);
1096         }
1097         $this->_filterModel[$this->_idProperty] = array('filter' => 'Tinebase_Model_Filter_Id', 'options' => array('idProperty' => $this->_idProperty, 'modelName' => $this->_appName . '_Model_' . $this->_modelName));
1098         $this->_fieldKeys = array_keys($this->_fields);
1099     }
1100
1101     /**
1102      * set filter model for field
1103      *
1104      * @param $fieldDef
1105      * @param $fieldKey
1106      */
1107     protected function _setFieldFilterModel($fieldDef, $fieldKey)
1108     {
1109         if (isset($fieldDef['filterDefinition'])) {
1110             // use filter from definition
1111             $key = isset($fieldDef['filterDefinition']['key']) ? $fieldDef['filterDefinition']['key'] : $fieldKey;
1112             if (isset($this->_filterModel[$key])) {
1113                 return;
1114             }
1115             $this->_filterModel[$key] = $fieldDef['filterDefinition'];
1116         } else {
1117             if (isset($this->_filterModel[$fieldKey])) {
1118                 return;
1119             }
1120             $type = $fieldDef['type'];
1121             $config = isset($fieldDef['config']) ? $fieldDef['config'] : null;
1122             if ('virtual' === $type && isset($fieldDef['config']) && isset($fieldDef['config']['type']) &&
1123                     'relation' === $fieldDef['config']['type']) {
1124                 $type = 'relation';
1125                 if (isset($config['config'])) {
1126                     $config = $config['config'];
1127                 }
1128                 if (isset($config['appName']) && isset($config['modelName'])) {
1129                     $config['related_model'] = $config['appName'] . '_Model_' . $config['modelName'];
1130                 }
1131                 if (!isset($config['own_model'])) {
1132                     $config['own_model'] = $this->_getPhpClassName();
1133                 }
1134             }
1135             if (isset($this->_filterModelMapping[$type])) {
1136                 // if no filterDefinition is given, try to use the default one
1137                 $this->_filterModel[$fieldKey] = array('filter' => $this->_filterModelMapping[$type]);
1138                 if (null !== $config) {
1139                     $this->_filterModel[$fieldKey]['options'] = $config;
1140
1141                     // set id filter controller
1142                     if ($type === 'record') {
1143                         $this->_filterModel[$fieldKey]['options']['filtergroup'] = $config['appName'] . '_Model_' . $config['modelName'] . 'Filter';
1144                         $this->_filterModel[$fieldKey]['options']['controller']  = $config['appName'] . '_Controller_' . $config['modelName'];
1145                     }
1146                 }
1147             }
1148         }
1149     }
1150
1151     /**
1152      * get table name for model
1153      *
1154      * @return string
1155      * @throws Tinebase_Exception_AccessDenied
1156      * @throws Tinebase_Exception_NotFound
1157      */
1158     protected function _getTableName()
1159     {
1160         if (is_array($this->_table) && isset($this->_table['name'])) {
1161             $tableName = $this->_table['name'];
1162         } else {
1163             // legacy way to find out table name, model conf should bring its table name
1164             $backend = Tinebase_Core::getApplicationInstance(
1165                 $this->_applicationName, $this->_modelName, /* $_ignoreACL */
1166                 true)->getBackend();
1167             $tableName = $backend->getTableName();
1168         }
1169
1170         return $tableName;
1171     }
1172
1173     /**
1174      * constructs the query filter
1175      *
1176      * adds ExplicitRelatedRecords-filters to query filter (relatedModels) to allow search in relations
1177      *
1178      * @param array $queryFilters
1179      *
1180      * @see 0011494: activate advanced search for contracts (customers, ...)
1181      */
1182     protected function _getQueryFilter($queryFilters)
1183     {
1184         $queryFilterData = array(
1185             'label' => 'Quick Search',
1186             'field' => 'query',
1187             'filter' => 'Tinebase_Model_Filter_Query',
1188             'useGlobalTranslation' => true,
1189             'options' => array(
1190                 'fields' => $queryFilters,
1191                 'modelName' => $this->_getPhpClassName(),
1192             )
1193         );
1194
1195         $relatedModels = array();
1196         foreach ($this->_filterModel as $name => $filter) {
1197             if ($filter['filter'] === 'Tinebase_Model_Filter_ExplicitRelatedRecord') {
1198                 $relatedModels[] = $filter['options']['related_model'];
1199             }
1200         }
1201         if (count($relatedModels) > 0) {
1202             $queryFilterData['options']['relatedModels'] = array_unique($relatedModels);
1203         }
1204
1205         $this->_filterModel['query'] = $queryFilterData;
1206     }
1207
1208     /**
1209      * get modelconfig for an array of models
1210      *
1211      * @param array $models
1212      * @param string $appname
1213      * @return array
1214      */
1215     public static function getFrontendConfigForModels($models, $appname = null)
1216     {
1217         $modelconfig = array();
1218         if (is_array($models)) {
1219             foreach ($models as $modelName) {
1220                 $recordClass = $appname ? $appname . '_Model_' . $modelName : $modelName;
1221                 $modelName = preg_replace('/^.+_Model_/', '', $modelName);
1222                 $config = $recordClass::getConfiguration();
1223                 if ($config) {
1224                     $modelconfig[$modelName] = $config->getFrontendConfiguration();
1225                 }
1226             }
1227         }
1228
1229         return $modelconfig;
1230     }
1231
1232     public function getAutoincrementFields()
1233     {
1234         return $this->_autoincrementFields;
1235     }
1236
1237     public function getIdProperty()
1238     {
1239         return $this->_idProperty;
1240     }
1241
1242     public function getTable()
1243     {
1244         return $this->_table;
1245     }
1246
1247     public function getVersion()
1248     {
1249         return $this->_version;
1250     }
1251
1252     public function getAppName()
1253     {
1254         return $this->_appName;
1255     }
1256
1257     public function getModelName()
1258     {
1259         return $this->_modelName;
1260     }
1261
1262     /**
1263      * populate model config properties
1264      * 
1265      * @param string $fieldKey
1266      * @param array $fieldDef
1267      */
1268     protected function _populateProperties($fieldKey, $fieldDef)
1269     {
1270         switch ($fieldDef['type']) {
1271             case 'string':
1272             case 'text':
1273             case 'fulltext':
1274             case 'integer':
1275             case 'float':
1276             case 'boolean':
1277                 break;
1278             case 'container':
1279                 break;
1280             case 'date':
1281                 // add to datetime fields
1282                 $this->_dateFields[] = $fieldKey;
1283                 break;
1284             case 'datetime':
1285                 // add to alarm fields
1286                 if ((isset($fieldDef['alarm']) || array_key_exists('alarm', $fieldDef))) {
1287                     $this->_alarmDateTimeField = $fieldKey;
1288                 }
1289                 // add to datetime fields
1290                 $this->_datetimeFields[] = $fieldKey;
1291                 break;
1292             case 'time':
1293                 // add to timefields
1294                 $this->_timeFields[] = $fieldKey;
1295                 break;
1296             case 'user':
1297                 $fieldDef['config'] = array(
1298                     'refIdField'              => 'id',
1299                     'length'                  => 40,
1300                     'appName'                 => 'Addressbook',
1301                     'modelName'               => 'Contact',
1302                     'recordClassName'         => 'Addressbook_Model_Contact',
1303                     'controllerClassName'     => 'Addressbook_Controller_Contact',
1304                     'filterClassName'         => 'Addressbook_Model_ContactFilter',
1305                     'addFilters' => array(
1306                         array('field' => 'type', 'operator' => 'equals', 'value' => 'user')
1307                     )
1308                 );
1309                 $this->_recordFields[$fieldKey] = $fieldDef;
1310                 break;
1311             case 'record':
1312                 $this->_filterModel[$fieldKey]['options']['controller']  = $this->_getPhpClassName($this->_filterModel[$fieldKey]['options'], 'Controller');
1313                 $this->_filterModel[$fieldKey]['options']['filtergroup'] = $this->_getPhpClassName($this->_filterModel[$fieldKey]['options'], 'Model') . 'Filter';
1314             case 'records':
1315                 $fieldDef['config']['recordClassName']     = (isset($fieldDef['config']['recordClassName']) || array_key_exists('recordClassName', $fieldDef['config']))     ? $fieldDef['config']['recordClassName']     : $this->_getPhpClassName($fieldDef['config']);
1316                 $fieldDef['config']['controllerClassName'] = (isset($fieldDef['config']['controllerClassName']) || array_key_exists('controllerClassName', $fieldDef['config'])) ? $fieldDef['config']['controllerClassName'] : $this->_getPhpClassName($fieldDef['config'], 'Controller');
1317                 $fieldDef['config']['filterClassName']     = (isset($fieldDef['config']['filterClassName']) || array_key_exists('filterClassName', $fieldDef['config']))     ? $fieldDef['config']['filterClassName']     : $this->_getPhpClassName($fieldDef['config']) . 'Filter';
1318                 if ($fieldDef['type'] == 'record') {
1319                     $fieldDef['config']['length'] = 40;
1320                     $this->_recordFields[$fieldKey] = $fieldDef;
1321                 } else {
1322                     $fieldDef['config']['dependentRecords'] = (isset($fieldDef['config']['dependentRecords']) || array_key_exists('dependentRecords', $fieldDef['config'])) ? $fieldDef['config']['dependentRecords'] : FALSE;
1323                     $this->_recordsFields[$fieldKey] = $fieldDef;
1324                 }
1325                 break;
1326             case 'custom':
1327                 try {
1328                     // prepend table name to id prop because of ambiguous ids
1329                     $this->_filterModel['customfield'] = array(
1330                         'filter' => 'Tinebase_Model_Filter_CustomField', 
1331                         'options' => array(
1332                             'idProperty' => $this->_getTableName() . '.' . $this->_idProperty
1333                         )
1334                     );
1335                 } catch (Exception $e) {
1336                     // no customfield filter available (yet?)
1337                     Tinebase_Exception::log($e);
1338                 }
1339                 break;
1340             default:
1341                 break;
1342         }
1343     }
1344     
1345     /**
1346      * returns an instance of the record controller
1347      * 
1348      * @return Tinebase_Controller_Record_Interface
1349      */
1350     public function getControllerInstance()
1351     {
1352         return Tinebase_Core::getApplicationInstance($this->_appName, $this->_modelName);
1353     }
1354     
1355     /**
1356      * gets phpClassName by field definition['config']
1357      *
1358      * @param array $_fieldConfig
1359      * @param string $_type
1360      * @return string
1361      */
1362     protected function _getPhpClassName($_fieldConfig = null, $_type = 'Model')
1363     {
1364         if (! $_fieldConfig) {
1365             $_fieldConfig = array('appName' => $this->_appName, 'modelName' => $this->_modelName);
1366         }
1367
1368         return $_fieldConfig['appName'] . '_' . $_type . '_' . $_fieldConfig['modelName'];
1369     }
1370     
1371     /**
1372      * checks if app and model is available for the user at record and records fields
1373      * - later this can be used to use field acl
1374      * - also checks feature switches
1375      * 
1376      * @param array $_fieldConfig the field configuration
1377      */
1378     protected function _isAvailable($_fieldConfig)
1379     {
1380         if (! (isset(self::$_availableApplications[$_fieldConfig['appName']]) || array_key_exists($_fieldConfig['appName'], self::$_availableApplications))) {
1381             self::$_availableApplications[$_fieldConfig['appName']] = Tinebase_Application::getInstance()->isInstalled($_fieldConfig['appName'], TRUE);
1382         }
1383         $result = self::$_availableApplications[$_fieldConfig['appName']];
1384
1385         if ($result && isset($_fieldConfig['feature'])) {
1386             $config = Tinebase_Config_Abstract::factory($_fieldConfig['appName']);
1387             $result = $config->featureEnabled($_fieldConfig['feature']);
1388
1389             if (! $result && Tinebase_Core::isLogLevel(Zend_Log::DEBUG)) Tinebase_Core::getLogger()->debug(__METHOD__ . '::' . __LINE__
1390                 . ' Feature ' . $_fieldConfig['feature'] . ' disables field');
1391         }
1392
1393         return $result;
1394     }
1395     
1396     /**
1397      * returns the filter configuration needed in the filtergroup for this model
1398      * 
1399      * @return array
1400      */
1401     public function getFilterModel()
1402     {
1403         // add calculated values to filter configuration
1404         if (! $this->_filterConfiguration) {
1405             foreach ($this->_filterProperties as $prop) {
1406                 $this->_filterConfiguration[$prop] = $this->{$prop};
1407             }
1408             // @todo: remove this as in the filtergroup
1409             $this->_filterConfiguration['_className'] = $this->_appName . '_Model_' . $this->_modelName . 'Filter';
1410         }
1411         return $this->_filterConfiguration;
1412     }
1413
1414     /**
1415      * returns the properties needed for the model
1416      * 
1417      * @return array
1418      */
1419     public function toArray()
1420     {
1421         if (! $this->_modelConfiguration) {
1422             // add calculated values to model configuration
1423             foreach ($this->_modelProperties as $prop) {
1424                 $this->_modelConfiguration[$prop] = $this->{$prop};
1425             }
1426         }
1427         
1428         return $this->_modelConfiguration;
1429     }
1430
1431     /**
1432      * returns the frontend configuration for creating js models, filters, defaults and some other js stubs.
1433      * this will be included in the registry.
1434      * Look at Tinebase/js/ApplicationStarter.js
1435      */
1436     public function getFrontendConfiguration()
1437     {
1438         if (! $this->_frontendConfiguration) {
1439             
1440             $this->_frontendConfiguration = array();
1441             
1442             // add calculated values to frontend configuration
1443             foreach ($this->_frontendProperties as $prop) {
1444                 $this->_frontendConfiguration[$prop] = $this->{'_' . $prop};
1445             }
1446         }
1447         return $this->_frontendConfiguration;
1448     }
1449
1450     /**
1451      * returns default data for this model
1452      * 
1453      * @return array
1454      */
1455     public function getDefaultData()
1456     {
1457         return $this->_defaultData;
1458     }
1459
1460     /**
1461      * returns the field configuration of the model
1462      */
1463     public function getFields()
1464     {
1465         return $this->_fields;
1466     }
1467
1468     /**
1469      * returns the converters of the model
1470      */
1471     public function getConverters()
1472     {
1473         return $this->_converters;
1474     }
1475
1476     /**
1477      * get protected property
1478      *
1479      * @param string name of property
1480      * @throws Tinebase_Exception_UnexpectedValue
1481      * @return mixed value of property
1482      */
1483     public function __get($_property)
1484     {
1485         if (! property_exists($this,  '_' . $_property)) {
1486             throw new Tinebase_Exception_UnexpectedValue('Property does not exist: ' . $_property);
1487         }
1488         return $this->{'_' . $_property};
1489     }
1490
1491     public static function resolveRecordsPropertiesForRecordSet(Tinebase_Record_RecordSet $_records, $modelConfiguration, $isSearch = false)
1492     {
1493         if (0 === $_records->count() || ! ($resolveFields = $modelConfiguration->recordsFields)) {
1494             return;
1495         }
1496
1497         $ownIds = $_records->{$modelConfiguration->idProperty};
1498
1499         // iterate fields to resolve
1500         foreach ($resolveFields as $fieldKey => $c) {
1501             $config = $c['config'];
1502
1503             // resolve records, if omitOnSearch is definitively set to FALSE (by default they won't be resolved on search)
1504             if ($isSearch && !(isset($config['omitOnSearch']) && $config['omitOnSearch'] === FALSE)) {
1505                 continue;
1506             }
1507
1508             if (! isset($config['controllerClassName'])) {
1509                 throw new Tinebase_Exception_UnexpectedValue('Controller class name needed');
1510             }
1511
1512             // fetch the fields by the refIfField
1513             /** @var Tinebase_Controller_Record_Interface|Tinebase_Controller_SearchInterface $controller */
1514             /** @noinspection PhpUndefinedMethodInspection */
1515             $controller = $config['controllerClassName']::getInstance();
1516             $filterName = $config['filterClassName'];
1517
1518             $filterArray = array();
1519
1520             // addFilters can be added and must be added if the same model resides in more than one records fields
1521             if (isset($config['addFilters']) && is_array($config['addFilters'])) {
1522                 $filterArray = $config['addFilters'];
1523             }
1524
1525             /** @var Tinebase_Model_Filter_FilterGroup $filter */
1526             $filter = new $filterName($filterArray);
1527             $filter->addFilter(new Tinebase_Model_Filter_Id(array('field' => $config['refIdField'], 'operator' => 'in', 'value' => $ownIds)));
1528
1529             $paging = NULL;
1530             if (isset($config['paging']) && is_array($config['paging'])) {
1531                 $paging = new Tinebase_Model_Pagination($config['paging']);
1532             }
1533
1534             $foreignRecords = $controller->search($filter, $paging);
1535             /** @var Tinebase_Record_Interface $foreignRecordClass */
1536             $foreignRecordClass = $foreignRecords->getRecordClassName();
1537             $foreignRecordModelConfiguration = $foreignRecordClass::getConfiguration();
1538
1539             $foreignRecords->setTimezone(Tinebase_Core::getUserTimezone());
1540             $foreignRecords->convertDates = true;
1541
1542             if ($foreignRecords->count() > 0) {
1543
1544                 // @todo: resolve alarms?
1545                 // @todo: use parts parameter?
1546                 if ($foreignRecordModelConfiguration->resolveRelated) {
1547                     $fr = $foreignRecords->getFirstRecord();
1548                     if ($fr->has('notes')) {
1549                         Tinebase_Notes::getInstance()->getMultipleNotesOfRecords($foreignRecords);
1550                     }
1551                     if ($fr->has('tags')) {
1552                         Tinebase_Tags::getInstance()->getMultipleTagsOfRecords($foreignRecords);
1553                     }
1554                     if ($fr->has('relations')) {
1555                         $relations = Tinebase_Relations::getInstance()->getMultipleRelations($foreignRecordClass, 'Sql', $foreignRecords->{$fr->getIdProperty()} );
1556                         $foreignRecords->setByIndices('relations', $relations);
1557                     }
1558                     if ($fr->has('customfields')) {
1559                         Tinebase_CustomField::getInstance()->resolveMultipleCustomfields($foreignRecords);
1560                     }
1561                     if ($fr->has('attachments') && Tinebase_Core::isFilesystemAvailable()) {
1562                         Tinebase_FileSystem_RecordAttachments::getInstance()->getMultipleAttachmentsOfRecords($foreignRecords);
1563                     }
1564                 }
1565
1566                 /** @var Tinebase_Record_Interface $record */
1567                 foreach ($_records as $record) {
1568                     $filtered = $foreignRecords->filter($config['refIdField'], $record->getId());
1569                     $record->{$fieldKey} = $filtered;
1570                 }
1571             }
1572
1573             foreach ($_records as $record) {
1574                 if (null === $record->{$fieldKey}) {
1575                     $record->{$fieldKey} = new Tinebase_Record_RecordSet($foreignRecordClass);
1576                 }
1577             }
1578         }
1579     }
1580
1581     /**
1582      * Returns all virtual fields
1583      *
1584      * @return array
1585      */
1586     public function getVirtualFields() {
1587         return $this->_virtualFields;
1588     }
1589 }