Merge branch 'pu/2013.03/modelconfig-hr'
[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 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 class Tinebase_ModelConfiguration {
20
21     /**
22      * this holds (caches) the availibility info of applications globally
23      * 
24      * @var array
25      */
26     static protected $_availableApplications = array('Tinebase' => TRUE);
27
28     /**
29      * the id property
30      *
31      * @var string
32      */
33     protected $_idProperty = 'id';
34     
35     // legacy
36     protected $_identifier;
37
38     /**
39      * Human readable name of the record
40      * add plural translation information in comments like:
41      * // ngettext('Record Name', 'Records Name', 1);
42      *
43      * @var string
44      */
45     protected $_recordName = NULL;
46
47     /**
48      * Human readable name of multiple records
49      * add plural translation information in comments like:
50      * // ngettext('Record Name', 'Records Name', 2);
51      *
52      * @var string
53      */
54     protected $_recordsName = NULL;
55
56     /**
57      * The property of the container, if any
58      *
59      * @var string
60      */
61     protected $_containerProperty = NULL;
62     
63     /**
64      * The property of the title, if any
65      *
66      * @var string
67      */
68     protected $_titleProperty = 'title';
69     
70     /**
71      * Human readable name of the container
72      * add plural translation information in comments like:
73      * // ngettext('Record Name', 'Records Name', 2);
74      *
75      * @var string
76      */
77     protected $_containerName = NULL;
78
79     /**
80      * Human readable name of multiple containers
81      * add plural translation information in comments like:
82      * // ngettext('Record Name', 'Records Name', 2);
83      *
84      * @var string
85      */
86     protected $_containersName = NULL;
87
88     /**
89      * If this is true, the record has relations
90      *
91      * @var boolean
92      */
93     protected $_hasRelations = NULL;
94
95     /**
96      * If this is true, the record has customfields
97      *
98      * @var boolean
99      */
100     protected $_hasCustomFields = NULL;
101
102     /**
103      * If this is true, the record has notes
104      *
105      * @var boolean
106      */
107     protected $_hasNotes = NULL;
108
109     /**
110      * If this is true, the record has tags
111      *
112      * @var boolean
113      */
114     protected $_hasTags = NULL;
115
116     /**
117      * If this is true, the record has file attachments
118      *
119      * @var boolean
120      */
121     protected $_hasAttachments = NULL;
122     
123     /**
124      * If this is true, a modlog will be created
125      *
126      * @var boolean
127      */
128     protected $_modlogActive = NULL;
129
130     /**
131      * If this is true, multiple edit of records of this model is possible.
132      *
133      * @var boolean
134      */
135     protected $_multipleEdit = NULL;
136
137     /**
138      * If multiple edit requires a special right, it's defined here
139      *
140      * @var string
141      */
142     protected $_multipleEditRequiredRight = NULL;
143
144     /**
145      * Group name of this model (will create a parent node in the modulepanel with this name)
146      * add translation information in comments like: // _('Group')
147      *
148      * @var string
149      */
150     protected $_moduleGroup = NULL;
151
152     /**
153      * Set the default Filter (defaults to query)
154      *
155      * @var string
156      */
157     protected $_defaultFilter = 'query';
158
159     /**
160      * Set the default sort info for the gridpanel (Tine.widgets.grid.GridPanel.defaultSortInfo)
161      * set as array('field' => 'title', 'direction' => 'DESC')
162      *
163      * @var array
164      */
165     protected $_defaultSortInfo = NULL;
166
167     /**
168      * Defines the right to see this model
169      *
170      * @var string
171      */
172     protected $_requiredRight = NULL;
173
174     /**
175      * no containers
176      * 
177      * @var boolean
178      */
179     protected $_singularContainerMode = NULL;
180
181     /**
182      * Holds the field definitions in an associative array where the key
183      * corresponds to the db-table name. Possible definitions and their defaults:
184      *
185      * - validators: Use Zend Input Filters to validate the values.
186      *       @type: Array, @default: array(Zend_Filter_Input::ALLOW_EMPTY => true)
187      *
188      * - 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.
189      *       Add translation information in comments like: // _('Title')
190      *       @type: String, @default: NULL
191      *
192      * - default: The default Value of the field.
193      *       Add translation information in comments like: // _('New Car')
194      *       @type: as defined (see DEFAULT MAPPING), @default: NULL
195      *
196      * - duplicateCheckGroup: All Fields having the same group will be combined for duplicate
197      *       check. If no group is given, no duplicate check will be done.
198      *       @type: String, @default: NULL
199      *
200      * - type: The type of the Value
201      *       @type: String, @default: "string"
202      *
203      * - specialType: Defines the type closer
204      *       @type: String, @default: NULL
205      *
206      * - filterDefinition: Overwrites the default filter used for this field
207      *       Definition knows all from Tinebase_Model_Filter_FilterGroup._filterModel
208      *       @type: Array, @default: array('filter' => 'Tinebase_Model_Filter_Text') or the default used for this type (see DEFAULT MAPPING)
209      * 
210      * - inputFilters: zend input filters to use for this field
211      *       @type: Array, use array(<InPutFilterClassName> => <constructorData>, ...)
212      * 
213      * - queryFilter: If this is set to true, this field will be used by the "query" filter
214      *       @type: Boolean, @default: NULL
215      *
216      * - duplicateOmit: Will neither be shown nor handled on duplicate resolving
217      *       @type: Boolean, @default: NULL
218      *
219      * - copyOmit: If this is set to true, the field won't be used on copy the record
220      *       @type: Boolean, @default: NULL
221      *
222      * - readOnly: If this is set to true, the field can't be updated and will be shown as readOnly in the frontend
223      *       @type: Boolean, @default: NULL
224      *
225      * - disabled: If this is set to true, the field can't be updated and will be shown as readOnly in the frontend
226      *       @type: Boolean, @default: NULL
227      *
228      * - group: Add this field to a group. Each group will be shown as a separate FieldSet of the
229      *       EditDialog and group in the DuplicateResolveGridPanel. If any field of this model
230      *       has a group set, FieldSets will be created and fields without a group set will be
231      *       added to a group with the same name as the RecordName.
232      *       Add translation information in comments like: // _('Group')
233      *       @type: String, @default: NULL
234      *
235      * - dateFormat: If type is a date, the format can be overwritten here
236      *       @type: String, @default: NULL or the default used for this type (see DEFAULT MAPPING)
237      *
238      * - shy: If this is set to true, the row for this field won't be shown in the grid, but can be activated
239      * 
240      * - sortable: If this is set to false, no sort by this field is possible in the gridpanel, defaults to true
241      * 
242      *   // TODO: generalize, currently only in ContractGridPanel, take it from there:
243      * - showInDetailsPanel: auto show in details panel if any is defined in the js gridpanel class
244      * 
245      * - useGlobalTranslation: if set, the global translation is used
246      * 
247      * DEFAULT MAPPING:
248      *
249      * Field-Type  specialType   Human-Type          SQL-Type JS-Type                       PHP-Type          PHP-Filter                  dateFormat    JS-FilterType
250      *
251      * date                      Date                datetime date                          Tinebase_DateTime Tinebase_Model_Filter_Date  ISO8601Short
252      * datetime                  Date with time      datetime date                          Tinebase_DateTime Tinebase_Model_Filter_Date  ISO8601Long
253      * time                      Time                datetime date                          Tinebase_DateTime Tinebase_Model_Filter_Date  ISO8601Time
254      * string                    Text                varchar  string                        string            Tinebase_Model_Filter_Text
255      * text                      Text with lnbr.     text     string                        string            Tinebase_Model_Filter_Text
256      * boolean                   Boolean             boolean  bool                          bool              Tinebase_Model_Filter_Bool
257      * integer                   Integer             integer  integer                       int               Tinebase_Model_Filter_Int                 number
258      * integer     bytes         Bytes               integer  integer                       int               Tinebase_Model_Filter_Int
259      * integer     usMoney       Dollar in Cent      integer  integer                       int               Tinebase_Model_Filter_Int
260      * integer     euMoney       Euro in Cent        integer  integer                       int               Tinebase_Model_Filter_Int
261      * integer     seconds       Seconds             integer  integer                       int               Tinebase_Model_Filter_Int
262      * integer     minutes       Minutes             integer  integer                       int               Tinebase_Model_Filter_Int
263      * float                     Float               float    float                         float             Tinebase_Model_Filter_Int
264      * container                 Container           string   Tine.Tinebase.Model.Container Tinebase_Model_Container                                    tine.widget.container.filtermodel
265      * tag tinebase.tag
266      * user                      User                string                                 Tinebase_Model_Filter_User
267      *
268      * * record/foreign (legacy) 1:1 - Relation      text     Tine.<APP>.Model.<MODEL>      Tinebase_Record_Abstract  Tinebase_Model_Filter_ForeignId   Tine.widgets.grid.ForeignRecordFilter
269      * * records                 1:n - Relation      -        Array of Record.data Objects  Tinebase_Record_RecordSet -                                 -
270      * * relation                m:m - Relation      -        Tinebase.Model.Relation       Tinebase_Model_Relation   Tinebase_Model_Filter_Relation
271      * * keyfield                String              string   <as defined>                  string            Tinebase_Model_Filter_Text
272      *
273      * * Accepts additional parameter: 'config' => array with these keys:
274      *     - @string appName    (the name of the application of the referenced record/s)
275      *     - @string modelName  (the name of the model of the referenced record/s)
276      *
277      *   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)
278      *     - @string recordClassName 
279      *     - @string controllerClassName
280      *     
281      *   Config for 'records' accepts also these keys:
282      *     - @string refIdField (the field of the foreign record referencing the idProperty of the own record)
283      *     - @array  paging     (accepts the parameters as Tinebase_Model_Pagination does)
284      *     - @string filterClassName
285      *     - @array  addFilters define them as array like Tinebase_Model_Filter_FilterGroup
286      * 
287      * record accepts keys additionally
288      *     - @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
289      * 
290      * <code>
291      *
292      * array(
293      *     'title' => array(
294      *         'validators' => array(Zend_Filter_Input::ALLOW_EMPTY => true),
295      *         'label' => NULL,
296      *         'duplicateCheckGroup' => 'number'
297      *     ),
298      *     'number' => ...
299      * )
300      *
301      * </code>
302      *
303      * @var array
304      */
305     protected $_fields = array();
306
307     /**
308      * holds all field definitions of type records
309      *
310      * @var array
311     */
312     protected $_recordsFields = NULL;
313
314     /**
315      * holds all field definitions of type record (foreignId fields)
316      *
317      * @var array
318      */
319     protected $_recordFields  = NULL;
320
321     /**
322      * holds virtual field definitions used for non-persistent fields getting calculated on each call of the record
323      * no backend property will be build, no filters etc. will exist. they must be filled in frontend json
324      * 
325      * @var array
326      */
327     protected $_virtualFields = NULL;
328     
329     /**
330      * maps fieldgroup keys to their names
331      * Add translation information in comments like: // _('Banking Information')
332      * 
333      * array(
334      *     'banking' => 'Banking Information',    // _('Banking Information')
335      *     'private' => 'Private Information',    // _('Private Information')
336      *     )
337      * 
338      * @var array
339      */
340     
341     protected $_fieldGroups = NULL;
342     /**
343      * here you can define one right (Tinebase_Acl_Rights_Abstract) for each field
344      * group ('group'-property of a field definition of this._fields), the user must
345      * have to see/edit this group, otherwise the fields of the edit dialog will be disabled/readOnly
346      *
347      * array(
348      *     'private' => array(
349      *         'see'  => HumanResources_Acl_Rights::SEE_PRIVATE,
350      *         'edit' => HumanResources_Acl_Rights::EDIT_PRIVATE,
351      *     ),
352      *     'banking' => array(
353      *         'see'  => HumanResources_Acl_Rights::SEE_BANKING,
354      *         'edit' => HumanResources_Acl_Rights::EDIT_BANKING,
355      *     )
356      * );
357      *
358      * @var array
359      */
360     protected $_fieldGroupRights = array();
361
362     /**
363      * every field group will be nested into a fieldset, here you can define the defaults (Ext.Container.defaults)
364      *
365      * @var array
366     */
367     protected $_fieldGroupFeDefaults = array();
368
369     protected $_createModule = FALSE;
370     
371     /*
372      * auto set by the constructor
373     */
374
375     /**
376      * If any field has a group, this will be set to true (autoset by the constructor)
377      *
378      * @var boolean
379     */
380     protected $_useGroups = FALSE;
381
382     /**
383      * the application this configuration belongs to (if the class has the name "Calendar_Model_Event", this will be resolved to "Calendar")
384      *
385      * @var string
386      */
387     protected $_appName = NULL;    // this should be used everytime, everywhere
388     // legacy
389     protected $_application = NULL;
390     protected $_applicationName = NULL;
391     /**
392      * the name of the model (if the class has the name "Calendar_Model_Event", this will be resolved to "Event")
393      *
394      * @var string
395      */
396     protected $_modelName = NULL;
397
398     /**
399      * holds the keys of all fields
400      *
401      * @var array
402      */
403     protected $_fieldKeys = array();
404
405     /**
406      * holds the time fields
407      *
408      * @var array
409     */
410     protected $_timeFields = array();
411
412     /**
413      * holds the fields which will be omitted in the modlog
414      *
415      * @var array
416     */
417     protected $_modlogOmitFields = array();
418
419     /**
420      * these fields will just be readOnly
421      *
422      * @var array
423     */
424     protected $_readOnlyFields = array();
425
426     /**
427      * holds the date and datetime fields
428      *
429      * @var array
430     */
431     protected $_datetimeFields = array();
432
433     /**
434      * holds the alarm datetime fields
435      *
436      * @var array
437     */
438     protected $_alarmDateTimeField = array();
439
440     /**
441      * The calculated filters for this model (auto set)
442      *
443      * @var array
444     */
445     protected $_filterModel = array();
446
447     /**
448      * holds the validators for the model (auto set)
449     */
450     protected $_validators = array();
451
452     /**
453      * holds validators which will be instanciated on model construction
454      *
455      * @var array
456     */
457     protected $_ownValidators = array();
458     
459     /**
460      * if a record is dependent to another, this is true
461      * 
462      * @var boolean
463      */
464     protected $_isDependent = FALSE;
465     
466     /**
467      * input filters (will be set by field configuration)
468      * 
469      * @var array
470      */
471     protected $_filters;
472     
473     /**
474      * Holds the default Data for the model (autoset from field config)
475      *
476      * @var array
477     */
478     protected $_defaultData = array();
479
480     /**
481      * holds the fields / groups to check for duplicates (will be auto set by field configuration)
482     */
483     protected $_duplicateCheckFields = NULL;
484
485     /**
486      * properties to collect for the filters (_appName and _modelName are set in the filter)
487      *
488      * @var array
489      */
490     protected $_filterProperties = array('_filterModel', '_defaultFilter', '_modelName', '_applicationName');
491
492     /**
493      * properties to collect for the model
494      *
495      * @var array
496     */
497     protected $_modelProperties = array(
498         '_identifier', '_timeFields', '_datetimeFields', '_alarmDateTimeField', '_validators', '_modlogOmitFields',
499         '_application', '_readOnlyFields', '_filters'
500     );
501
502     /**
503      * properties to collect for the frontend
504      *
505      * @var array
506     */
507     protected $_frontendProperties = array(
508         'containerProperty', 'containersName', 'containerName', 'defaultSortInfo', 'fieldKeys', 'filterModel',
509         'defaultFilter', 'requiredRight', 'singularContainerMode', 'fields', 'defaultData', 'titleProperty',
510         'useGroups', 'fieldGroupFeDefaults', 'fieldGroupRights', 'multipleEdit', 'multipleEditRequiredRight',
511         'recordName', 'recordsName', 'appName', 'modelName', 'createModule', 'virtualFields', 'group', 'isDependent',
512         'hasCustomFields', 'modlogActive', 'hasAttachments'
513     );
514
515     /**
516      * the module group (will be on the same leaf of the content type tree panel)
517      * 
518      * @var string
519      */
520     protected $_group = NULL;
521     
522     /**
523      * the backend properties holding the collected properties
524      *
525      * @var array
526     */
527     protected $_modelConfiguration = NULL;
528
529     /**
530      * holds the collected values for the frontendconfig (autoset on first call of getFrontendConfiguration)
531      * 
532      * @var array
533      */
534     protected $_frontendConfiguration = NULL;
535     
536     /**
537      * the backend properties holding the collected properties
538      *
539      * @var array
540      */
541     protected $_filterConfiguration = NULL;
542
543     /*
544      * mappings
545      */
546
547     /**
548      * This defines the filters use for all known types
549      * @var array
550      */
551     protected $_filterModelMapping = array(
552         'date'     => 'Tinebase_Model_Filter_Date',
553         'datetime' => 'Tinebase_Model_Filter_Date',
554         'time'     => 'Tinebase_Model_Filter_Date',
555         'string'   => 'Tinebase_Model_Filter_Text',
556         'text'     => 'Tinebase_Model_Filter_Text',
557         'boolean'  => 'Tinebase_Model_Filter_Bool',
558         'integer'  => 'Tinebase_Model_Filter_Int',
559         'float'    => 'Tinebase_Model_Filter_Int',
560         'record'   => 'Tinebase_Model_Filter_ForeignId',
561         'relation' => 'Tinebase_Model_Filter_Relation',
562
563         'keyfield'  => 'Tinebase_Model_Filter_Text',
564         'container' => 'Tinebase_Model_Filter_Container',
565         'tag'       => 'Tinebase_Model_Filter_Tag',
566         'user'      => 'Tinebase_Model_Filter_User',
567     );
568
569     /**
570      * This maps field types to own validators, which will be instanciated in the constructor.
571      *
572      * @var array
573     */
574     protected $_inputFilterDefaultMapping = array(
575         'text'     => array('Tinebase_Model_InputFilter_CrlfConvert'),
576     );
577
578     /**
579      * This maps field types to their default validators, just zendfw validators can be used here.
580      * For using own validators, use _ownValidatorMapping instead. If no validator is given,
581      * "array(Zend_Filter_Input::ALLOW_EMPTY => true)" will be used
582      *
583      * @var array
584     */
585     protected $_validatorMapping = array(
586         'record'    => array(Zend_Filter_Input::ALLOW_EMPTY => true, Zend_Filter_Input::DEFAULT_VALUE => NULL),
587         'relation'  => array(Zend_Filter_Input::ALLOW_EMPTY => true, Zend_Filter_Input::DEFAULT_VALUE => NULL),
588     );
589
590     /**
591      * the constructor (must be called by the singleton pattern)
592      *
593      * @var array $modelClassConfiguration
594      * @throws Tinebase_Exception_Record_DefinitionFailure
595      */
596     public function __construct($modelClassConfiguration)
597     {
598         if (! $modelClassConfiguration) {
599             throw new Tinebase_Exception('The model class configuration must be submitted!');
600         }
601
602         $this->_appName     = $this->_application = $this->_applicationName = $modelClassConfiguration['appName'];
603         
604         // add appName to available applications 
605         self::$_availableApplications[$this->_appName] = TRUE;
606         
607         $this->_modelName   = $modelClassConfiguration['modelName'];
608         $this->_idProperty  = $this->_identifier = array_key_exists('idProperty', $modelClassConfiguration) ? $modelClassConfiguration['idProperty'] : 'id';
609
610         // some cruid validating
611         foreach ($modelClassConfiguration as $propertyName => $propertyValue) {
612             $this->{'_' . $propertyName} = $propertyValue;
613         }
614         
615         $this->_filters = array();
616         $this->_fields[$this->_idProperty] = array('label' => NULL, 'validators' => array(Zend_Filter_Input::ALLOW_EMPTY => true));
617
618         if ($this->_hasCustomFields) {
619             $this->_fields['customfields'] = array('label' => NULL, 'type' => 'custom', 'validators' => array(Zend_Filter_Input::ALLOW_EMPTY => true, Zend_Filter_Input::DEFAULT_VALUE => NULL));
620         }
621
622         if ($this->_hasRelations) {
623             $this->_fields['relations'] = array('label' => NULL, 'type' => 'relation', 'validators' => array(Zend_Filter_Input::ALLOW_EMPTY => true, Zend_Filter_Input::DEFAULT_VALUE => NULL));
624         }
625
626         if ($this->_containerProperty) {
627             $this->_fields[$this->_containerProperty] = array(
628                 'label'            => $this->_containerName,
629                 'shy'              => true,
630                 'type'             => 'container',
631                 'validators'       => array(Zend_Filter_Input::ALLOW_EMPTY => true),
632                 'filterDefinition' => array(
633                     'filter'  => $this->_filterModelMapping['container'],
634                     'options' => array('applicationName' => $this->_appName)
635                 )
636             );
637         } else {
638             $this->_singularContainerMode = true;
639         }
640
641         if ($this->_hasTags) {
642             $this->_fields['tags'] = array(
643                 'label' => 'Tags',
644                 'sortable' => false,
645                 'type' => 'tag', 
646                 'validators' => array(Zend_Filter_Input::ALLOW_EMPTY => true, Zend_Filter_Input::DEFAULT_VALUE => NULL), 
647                 'useGlobalTranslation' => TRUE
648             );
649         }
650
651         if ($this->_hasAttachments) {
652             $this->_fields['attachments'] = array(
653                 'label' => NULL,
654                 'type'  => 'attachments'
655             );
656         }
657         
658         
659         if ($this->_modlogActive) {
660             // notes are needed if modlog is active
661             $this->_fields['notes']              = array('label' => NULL,                 'type' => 'note',     'validators' => array(Zend_Filter_Input::ALLOW_EMPTY => true, Zend_Filter_Input::DEFAULT_VALUE => NULL), 'useGlobalTranslation' => TRUE);
662             $this->_fields['created_by']         = array('label' => 'Created By',         'type' => 'user',     'validators' => array(Zend_Filter_Input::ALLOW_EMPTY => true), 'shy' => true, 'useGlobalTranslation' => TRUE);
663             $this->_fields['creation_time']      = array('label' => 'Creation Time',      'type' => 'datetime', 'validators' => array(Zend_Filter_Input::ALLOW_EMPTY => true), 'shy' => true, 'useGlobalTranslation' => TRUE);
664             $this->_fields['last_modified_by']   = array('label' => 'Last Modified By',   'type' => 'user',     'validators' => array(Zend_Filter_Input::ALLOW_EMPTY => true), 'shy' => true, 'useGlobalTranslation' => TRUE);
665             $this->_fields['last_modified_time'] = array('label' => 'Last Modified Time', 'type' => 'datetime', 'validators' => array(Zend_Filter_Input::ALLOW_EMPTY => true), 'shy' => true, 'useGlobalTranslation' => TRUE);
666
667             // don't show deleted information
668             $this->_fields['deleted_by']         = array('label' => NULL, 'type' => 'user',     'validators' => array(Zend_Filter_Input::ALLOW_EMPTY => true), 'useGlobalTranslation' => TRUE);
669             $this->_fields['deleted_time']       = array('label' => NULL, 'type' => 'datetime', 'validators' => array(Zend_Filter_Input::ALLOW_EMPTY => true), 'useGlobalTranslation' => TRUE);
670             $this->_fields['is_deleted']         = array('label' => NULL, 'type' => 'boolean',  'validators' => array(Zend_Filter_Input::ALLOW_EMPTY => true), 'useGlobalTranslation' => TRUE);
671
672         } elseif ($this->_hasNotes) {
673             $this->_fields['notes'] = array('label' => NULL, 'type' => 'note', 'validators' => array(Zend_Filter_Input::ALLOW_EMPTY => true, Zend_Filter_Input::DEFAULT_VALUE => NULL));
674         }
675         
676         // holds the filters used for the query-filter, if any
677         $queryFilters = array();
678         
679         foreach ($this->_fields as $fieldKey => &$fieldDef) {
680             // set default type to string, if no type is given
681             if (! array_key_exists('type', $fieldDef)) {
682                 $fieldDef['type'] = 'string';
683             }
684             
685             // don't handle field if app is not available
686             if (array_key_exists('config', $fieldDef) && ($fieldDef['type'] == 'record' || $fieldDef['type'] == 'records') && (! $this->_isAvailable($fieldDef['config']))) {
687                 $fieldDef['type'] = 'string';
688                 $fieldDef['label'] = NULL;
689                 continue;
690             }
691             // the property name
692             $fieldDef['key'] = $fieldKey;
693
694             // if any field has a group set, enable grouping globally
695             if (! $this->_useGroups && array_key_exists('group', $fieldDef)) {
696                 $this->_useGroups = TRUE;
697             }
698
699             if ($fieldDef['type'] == 'virtual') {
700                 $fieldDef = $fieldDef['config'];
701                 $fieldDef['sortable'] = FALSE;
702                 $this->_virtualFields[] = $fieldDef;
703                 continue;
704             }
705             
706             // set default value
707             // TODO: implement complex default values
708             if (array_key_exists('default', $fieldDef)) {
709 //                 // allows dynamic default values
710 //                 if (is_array($fieldDef['default'])) {
711 //                     switch ($fieldDef['type']) {
712 //                         case 'time':
713 //                         case 'date':
714 //                         case 'datetime':
715 //                         default:
716 //                             throw new Tinebase_Exception_NotImplemented($_message);
717 //                     }
718 //                 } else {
719                     $this->_defaultData[$fieldKey] = $fieldDef['default'];
720                     
721 //                 }
722             }
723
724             // set filter model
725             if (array_key_exists('filterDefinition', $fieldDef)) {
726                 // use filter from definition
727                 $this->_filterModel[$fieldKey] = $fieldDef['filterDefinition'];
728             } else if (array_key_exists($fieldDef['type'], $this->_filterModelMapping)) {
729                 // if no filterDefinition is given, try to use the default one
730                 $this->_filterModel[$fieldKey] = array('filter' => $this->_filterModelMapping[$fieldDef['type']]);
731                 if (array_key_exists('config', $fieldDef)) {
732                     $this->_filterModel[$fieldKey]['options'] = $fieldDef['config'];
733                     
734                     // set id filter controller
735                     if ($fieldDef['type'] == 'record') {
736                         $this->_filterModel[$fieldKey]['options']['filtergroup'] = $fieldDef['config']['appName'] . '_Model_' . $fieldDef['config']['modelName'] . 'Filter';
737                         $this->_filterModel[$fieldKey]['options']['controller']  = $fieldDef['config']['appName'] . '_Controller_' . $fieldDef['config']['modelName'];
738                     }
739                     
740                 }
741             }
742             
743             if (array_key_exists('queryFilter', $fieldDef)) {
744                 $queryFilters[] = $fieldKey;
745             }
746
747             // set validators
748             if (array_key_exists('validators', $fieldDef)) {
749                 // use _validators from definition
750                 $this->_validators[$fieldKey] = $fieldDef['validators'];
751             } else if (array_key_exists($fieldDef['type'], $this->_validatorMapping)) {
752                 // if no validatorsDefinition is given, try to use the default one
753                 $fieldDef['validators'] = $this->_validators[$fieldKey] = $this->_validatorMapping[$fieldDef['type']];
754             } else {
755                 $fieldDef['validators'] = $this->_validators[$fieldKey] = array(Zend_Filter_Input::ALLOW_EMPTY => true);
756             }
757
758             // set input filters, append defined if any or use defaults from _inputFilterDefaultMapping 
759             if (array_key_exists('inputFilters', $fieldDef)) {
760                 foreach ($fieldDef['inputFilters'] as $if => $val) { 
761                     $this->_filters[$fieldKey][] = $if ? new $if($val) : new $val();
762                 }
763             } else if (array_key_exists($fieldDef['type'], $this->_inputFilterDefaultMapping)) {
764                 foreach ($this->_inputFilterDefaultMapping[$fieldDef['type']] as $if => $val) {
765                     $this->_filters[$fieldKey][] = $if ? new $if($val) : new $val();
766                 }
767             }
768             
769             // add field to modlog omit, if configured and modlog is used
770             if ($this->_modlogActive && array_key_exists('modlogOmit', $fieldDef)) {
771                 $this->_modlogOmitFields[] = $fieldKey;
772             }
773
774             // populate properties
775             switch ($fieldDef['type']) {
776                 case 'string':
777                 case 'text':
778                 case 'integer':
779                 case 'float':
780                 case 'boolean':
781                     break;
782                 case 'container':
783                     break;
784                 case 'date':
785                 case 'datetime':
786                     // add to alarm fields
787                     if (array_key_exists('alarm', $fieldDef)) {
788                         $this->_alarmDateTimeField = $fieldKey;
789                     }
790                     // add to datetime fields
791                     $this->_datetimeFields[] = $fieldKey;
792                     break;
793                 case 'time':
794                     // add to timefields
795                     $this->_timeFields[] = $fieldKey;
796                     break;
797                 case 'user':
798                     $fieldDef['config'] = array(
799                         'refIdField'              => 'id',
800                         'appName'                 => 'Addressbook',
801                         'modelName'               => 'Contact',
802                         'recordClassName'         => 'Addressbook_Model_Contact',
803                         'controllerClassName'     => 'Addressbook_Controller_Contact',
804                         'filterClassName'         => 'Addressbook_Model_ContactFilter',
805                         'addFilters' => array(
806                             array('field' => 'type', 'operator' => 'equals', 'value' => 'user')
807                         )
808                     );
809                     $this->_recordFields[$fieldKey] = $fieldDef;
810                     break;
811                 case 'record':
812                     $this->_filterModel[$fieldKey]['options']['controller']  = $this->_getPhpClassName($this->_filterModel[$fieldKey]['options'], 'Controller');
813                     $this->_filterModel[$fieldKey]['options']['filtergroup'] = $this->_getPhpClassName($this->_filterModel[$fieldKey]['options'], 'Model') . 'Filter';
814                 case 'records':
815                     $fieldDef['config']['recordClassName']     = array_key_exists('recordClassName', $fieldDef['config'])     ? $fieldDef['config']['recordClassName']     : $this->_getPhpClassName($fieldDef['config']);
816                     $fieldDef['config']['controllerClassName'] = array_key_exists('controllerClassName', $fieldDef['config']) ? $fieldDef['config']['controllerClassName'] : $this->_getPhpClassName($fieldDef['config'], 'Controller');
817                     $fieldDef['config']['filterClassName']     = array_key_exists('filterClassName', $fieldDef['config'])     ? $fieldDef['config']['filterClassName']     : $this->_getPhpClassName($fieldDef['config']) . 'Filter';
818                     if ($fieldDef['type'] == 'record') {
819                         $this->_recordFields[$fieldKey] = $fieldDef;
820                     } else {
821                         $fieldDef['config']['dependentRecords'] = array_key_exists('dependentRecords', $fieldDef['config']) ? $fieldDef['config']['dependentRecords'] : FALSE;
822                         $this->_recordsFields[$fieldKey] = $fieldDef;
823                     }
824                     break;
825                 default:
826                     break;
827             }
828         }
829         
830         // set some default filters
831         if (count($queryFilters)) {
832             $this->_filterModel['query'] = array('label' => 'Quick Search', 'field' => 'query', 'filter' => 'Tinebase_Model_Filter_Query', 'options' => array('fields' => $queryFilters), 'useGlobalTranslation' => true);
833         }
834         $this->_filterModel[$this->_idProperty] = array('filter' => 'Tinebase_Model_Filter_Id', 'options' => array('modelName' => $this->_appName . '_Model_' . $this->_modelName));
835         $this->_fieldKeys = array_keys($this->_fields);
836     }
837     
838     /**
839      * gets phpClassName by field definition['config']
840      *
841      * @param array $_fieldConfig
842      * @param string $_type
843      * @return string
844      */
845     protected function _getPhpClassName($_fieldConfig, $_type = 'Model')
846     {
847         return $_fieldConfig['appName'] . '_' . $_type . '_' . $_fieldConfig['modelName'];
848     }
849     
850     /**
851      * checks if app and model is available for the user at record and records fields
852      * later this can be used to use field acl
853      * 
854      * @param array $_fieldConfig the field configuration
855      */
856     protected function _isAvailable($_fieldConfig)
857     {
858         if (! array_key_exists($_fieldConfig['appName'], self::$_availableApplications)) {
859             self::$_availableApplications[$_fieldConfig['appName']] = Tinebase_Application::getInstance()->isInstalled($_fieldConfig['appName'], TRUE);
860         }
861         return self::$_availableApplications[$_fieldConfig['appName']];
862     }
863     
864     /**
865      * returns the filterconfiguration needed in the filtergroup for this model
866      * 
867      * @return array
868      */
869     public function getFilterModel()
870     {
871         // add calculated values to filter configuration
872         if (! $this->_filterConfiguration) {
873             foreach ($this->_filterProperties as $prop) {
874                 $this->_filterConfiguration[$prop] = $this->{$prop};
875             }
876             // @todo: remove this as in the filtergroup
877             $this->_filterConfiguration['_className'] = $this->_appName . '_Model_' . $this->_modelName . 'Filter';
878         }
879         return $this->_filterConfiguration;
880     }
881
882     /**
883      * returns the properties needed for the model
884      * 
885      * @return array
886      */
887     public function toArray()
888     {
889         if (! $this->_modelConfiguration) {
890             // add calculated values to model configuration
891             foreach ($this->_modelProperties as $prop) {
892                 $this->_modelConfiguration[$prop] = $this->{$prop};
893             }
894         }
895         
896         return $this->_modelConfiguration;
897     }
898
899     /**
900      * returns the frontend configuration for creating js models, filters, defaults and some other js stubs.
901      * this will be included in the registry.
902      * Look at Tinebase/js/ApplicationStarter.js
903      */
904     public function getFrontendConfiguration()
905     {
906         if (! $this->_frontendConfiguration) {
907             
908             $this->_frontendConfiguration = array();
909             
910             // add calculated values to frontend configuration
911             foreach ($this->_frontendProperties as $prop) {
912                 $this->_frontendConfiguration[$prop] = $this->{'_' . $prop};
913             }
914         }
915         return $this->_frontendConfiguration;
916     }
917
918     /**
919      * returns default data for this model
920      * 
921      * @return array
922      */
923     public function getDefaultData()
924     {
925         return $this->_defaultData;
926     }
927
928     /**
929      * returns the field configuration of the model
930      */
931     public function getFields()
932     {
933         return $this->_fields;
934     }
935
936     /**
937      * get protected property
938      *
939      * @param string name of property
940      * @throws Tinebase_Exception_UnexpectedValue
941      * @return mixed value of property
942      */
943     public function __get($_property)
944     {
945         if (! property_exists($this,  '_' . $_property)) {
946             throw new Tinebase_Exception_UnexpectedValue('Property does not exist: ' . $_property);
947         }
948         return $this->{'_' . $_property};
949     }
950 }