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