/**
  * Return the specific attribute with the specified attribute_id (assuming it's attached to the current row)
  */
 public function getAttributeByID($pn_attribute_id, $pa_options = null)
 {
     if (isset($pa_options['row_id']) && $pa_options['row_id']) {
         $vn_row_id = $pa_options['row_id'];
     } else {
         $vn_row_id = $this->getPrimaryKey();
     }
     if (!$vn_row_id) {
         return null;
     }
     $t_attr = new ca_attributes($pn_attribute_id);
     if (!$t_attr->getPrimaryKey()) {
         return false;
     }
     if ((int) $t_attr->get('row_id') !== (int) $vn_row_id) {
         return false;
     }
     return $t_attr->getAttributeValues(array('returnAs' => 'attributeInstance'));
 }
 /**
  * Processes single exporter item for a given record
  *
  * @param int $pn_item_id Primary of exporter item
  * @param int $pn_table_num Table num of item to export
  * @param int $pn_record_id Primary key value of item to export
  * @param array $pa_options
  *		ignoreContext = don't switch context even though context may be set for current item
  *		relationship_type_id, relationship_type_code, relationship_typename =
  *			if this export is a sub-export (context-switch), we have no way of knowing the relationship
  *			to the 'parent' element in the export, so there has to be a means to pass it down to make it accessible
  * 		attribute_id = signals that this is an export relative to a specific attribute instance
  * 			this triggers special behavior that allows getting container values in a kind of sub-export
  *			it's really only useful for Containers but in theory can be any attribute
  *		logger = KLogger instance to use for logging. This option is mandatory!
  * @return array Item info
  */
 public function processExporterItem($pn_item_id, $pn_table_num, $pn_record_id, $pa_options = array())
 {
     $o_log = caGetOption('logger', $pa_options);
     // always set by exportRecord()
     $vb_ignore_context = caGetOption('ignoreContext', $pa_options);
     $vn_attribute_id = caGetOption('attribute_id', $pa_options);
     $o_log->logInfo(_t("Export mapping processor called with parameters [exporter_item_id:%1 table_num:%2 record_id:%3]", $pn_item_id, $pn_table_num, $pn_record_id));
     $t_exporter_item = ca_data_exporters::loadExporterItemByID($pn_item_id);
     $t_instance = ca_data_exporters::loadInstanceByID($pn_record_id, $pn_table_num);
     // switch context to a different set of records if necessary and repeat current exporter item for all those selected records
     // (e.g. hierarchy children or related items in another table, restricted by types or relationship types)
     if (!$vb_ignore_context && ($vs_context = $t_exporter_item->getSetting('context'))) {
         $va_restrict_to_types = $t_exporter_item->getSetting('restrictToTypes');
         $va_restrict_to_rel_types = $t_exporter_item->getSetting('restrictToRelationshipTypes');
         $va_restrict_to_bundle_vals = $t_exporter_item->getSetting('restrictToBundleValues');
         $va_check_access = $t_exporter_item->getSetting('checkAccess');
         $va_sort = $t_exporter_item->getSetting('sort');
         $vn_new_table_num = $this->getAppDatamodel()->getTableNum($vs_context);
         $vb_context_is_related_table = false;
         $va_related = null;
         if ($vn_new_table_num) {
             // switch to new table
             $vs_key = $this->getAppDatamodel()->getTablePrimaryKeyName($vs_context);
         } else {
             // this table, i.e. hierarchy context switch
             $vs_key = $t_instance->primaryKey();
         }
         $o_log->logInfo(_t("Initiating context switch to '%1' for mapping ID %2 and record ID %3. The processor now tries to find matching records for the switch and calls itself for each of those items.", $vs_context, $pn_item_id, $pn_record_id));
         switch ($vs_context) {
             case 'children':
                 $va_related = $t_instance->getHierarchyChildren();
                 break;
             case 'parent':
                 $va_related = array();
                 if ($vs_parent_id_fld = $t_instance->getProperty("HIERARCHY_PARENT_ID_FLD")) {
                     $va_related[] = array($vs_key => $t_instance->get($vs_parent_id_fld));
                 }
                 break;
             case 'ancestors':
                 $va_parents = $t_instance->getHierarchyAncestors(null, array('idsOnly' => true));
                 $va_related = array();
                 foreach (array_unique($va_parents) as $vn_pk) {
                     $va_related[] = array($vs_key => intval($vn_pk));
                 }
                 break;
             case 'ca_sets':
                 $t_set = new ca_sets();
                 $va_set_options = array();
                 if (isset($va_restrict_to_types[0])) {
                     // the utility used below doesn't support passing multiple types so we just pass the first.
                     // this should be enough for 99.99% of the actual use cases anyway
                     $va_set_options['setType'] = $va_restrict_to_types[0];
                 }
                 $va_set_options['checkAccess'] = $va_check_access;
                 $va_set_options['setIDsOnly'] = true;
                 $va_set_ids = $t_set->getSetsForItem($pn_table_num, $t_instance->getPrimaryKey(), $va_set_options);
                 $va_related = array();
                 foreach (array_unique($va_set_ids) as $vn_pk) {
                     $va_related[] = array($vs_key => intval($vn_pk));
                 }
                 break;
             case 'ca_list_items.firstLevel':
                 if ($t_instance->tableName() == 'ca_lists') {
                     $o_dm = Datamodel::load();
                     $va_related = array();
                     $va_items_legacy_format = $t_instance->getListItemsAsHierarchy(null, array('maxLevels' => 1, 'dontIncludeRoot' => true));
                     $vn_new_table_num = $o_dm->getTableNum('ca_list_items');
                     $vs_key = 'item_id';
                     foreach ($va_items_legacy_format as $va_item_legacy_format) {
                         $va_related[$va_item_legacy_format['NODE']['item_id']] = $va_item_legacy_format['NODE'];
                     }
                     break;
                 } else {
                     return array();
                 }
                 break;
             default:
                 if ($vn_new_table_num) {
                     $va_options = array('restrictToTypes' => $va_restrict_to_types, 'restrictToRelationshipTypes' => $va_restrict_to_rel_types, 'restrictToBundleValues' => $va_restrict_to_bundle_vals, 'checkAccess' => $va_check_access, 'sort' => $va_sort);
                     $o_log->logDebug(_t("Calling getRelatedItems with options: %1.", print_r($va_options, true)));
                     $va_related = $t_instance->getRelatedItems($vs_context, $va_options);
                     $vb_context_is_related_table = true;
                 } else {
                     // container or invalid context
                     $va_context_tmp = explode('.', $vs_context);
                     if (sizeof($va_context_tmp) != 2) {
                         $o_log->logError(_t("Invalid context %1. Ignoring this mapping.", $vs_context));
                         return array();
                     }
                     $va_attrs = $t_instance->getAttributesByElement($va_context_tmp[1]);
                     $va_info = array();
                     if (is_array($va_attrs) && sizeof($va_attrs) > 0) {
                         $o_log->logInfo(_t("Switching context for element code: %1.", $va_context_tmp[1]));
                         $o_log->logDebug(_t("Raw attribute value array is as follows. The mapping will now be repeated for each (outer) attribute. %1", print_r($va_attrs, true)));
                         foreach ($va_attrs as $vo_attr) {
                             $va_attribute_export = $this->processExporterItem($pn_item_id, $pn_table_num, $pn_record_id, array_merge(array('ignoreContext' => true, 'attribute_id' => $vo_attr->getAttributeID()), $pa_options));
                             $va_info = array_merge($va_info, $va_attribute_export);
                         }
                     } else {
                         $o_log->logInfo(_t("Switching context for element code %1 failed. Either there is no attribute with that code attached to the current row or the code is invalid. Mapping is ignored for current row.", $va_context_tmp[1]));
                     }
                     return $va_info;
                 }
                 break;
         }
         $va_info = array();
         if (is_array($va_related)) {
             $o_log->logDebug(_t("The current mapping will now be repreated for these items: %1", print_r($va_related, true)));
             if (!$vn_new_table_num) {
                 $vn_new_table_num = $pn_table_num;
             }
             foreach ($va_related as $va_rel) {
                 // if we're dealing with a related table, pass on some info the relationship type to the context-switched invocation of processExporterItem(),
                 // because we can't access that information from the related item simply because we don't exactly know where the call originated
                 if ($vb_context_is_related_table) {
                     $pa_options['relationship_typename'] = $va_rel['relationship_typename'];
                     $pa_options['relationship_type_code'] = $va_rel['relationship_type_code'];
                     $pa_options['relationship_type_id'] = $va_rel['relationship_type_id'];
                 }
                 $va_rel_export = $this->processExporterItem($pn_item_id, $vn_new_table_num, $va_rel[$vs_key], array_merge(array('ignoreContext' => true), $pa_options));
                 $va_info = array_merge($va_info, $va_rel_export);
             }
         } else {
             $o_log->logDebug(_t("No matching related items found for last context switch"));
         }
         return $va_info;
     }
     // end switch context
     // Don't prevent context switches for children of context-switched exporter items. This way you can
     // build cascades for jobs like exporting objects related to the creator of the record in question.
     unset($pa_options['ignoreContext']);
     $va_item_info = array();
     $vs_source = $t_exporter_item->get('source');
     $vs_element = $t_exporter_item->get('element');
     $vb_repeat = $t_exporter_item->getSetting('repeat_element_for_multiple_values');
     // if omitIfEmpty is set and get() returns nothing, we ignore this exporter item and all children
     if ($vs_omit_if_empty = $t_exporter_item->getSetting('omitIfEmpty')) {
         if (!(strlen($t_instance->get($vs_omit_if_empty)) > 0)) {
             return array();
         }
     }
     // if omitIfNotEmpty is set and get() returns a value, we ignore this exporter item and all children
     if ($vs_omit_if_not_empty = $t_exporter_item->getSetting('omitIfNotEmpty')) {
         if (strlen($t_instance->get($vs_omit_if_not_empty)) > 0) {
             return array();
         }
     }
     // always return URL for export, not an HTML tag
     $va_get_options = array('returnURL' => true);
     if ($vs_delimiter = $t_exporter_item->getSetting("delimiter")) {
         $va_get_options['delimiter'] = $vs_delimiter;
     }
     if ($vs_template = $t_exporter_item->getSetting('template')) {
         $va_get_options['template'] = $vs_template;
     }
     if ($vs_locale = $t_exporter_item->getSetting('locale')) {
         // the global UI locale for some reason has a higher priority
         // than the locale setting in BaseModelWithAttributes::get
         // which is why we unset it here and restore it later
         global $g_ui_locale;
         $vs_old_ui_locale = $g_ui_locale;
         $g_ui_locale = null;
         $va_get_options['locale'] = $vs_locale;
     }
     // AttributeValue settings that are simply passed through by the exporter
     if ($t_exporter_item->getSetting('convertCodesToDisplayText')) {
         $va_get_options['convertCodesToDisplayText'] = true;
         // try to return text suitable for display for system lists stored in intrinsics (ex. ca_objects.access, ca_objects.status, ca_objects.source_id)
         // this does not affect list attributes
     } else {
         $va_get_options['convertCodesToIdno'] = true;
         // if display text is not requested try to return list item idno's... since underlying integer ca_list_items.item_id values are unlikely to be useful in an export context
     }
     if ($t_exporter_item->getSetting('returnIdno')) {
         $va_get_options['returnIdno'] = true;
     }
     if ($t_exporter_item->getSetting('start_as_iso8601')) {
         $va_get_options['start_as_iso8601'] = true;
     }
     if ($t_exporter_item->getSetting('end_as_iso8601')) {
         $va_get_options['end_as_iso8601'] = true;
     }
     if ($t_exporter_item->getSetting('dontReturnValueIfOnSameDayAsStart')) {
         $va_get_options['dontReturnValueIfOnSameDayAsStart'] = true;
     }
     if ($vs_date_format = $t_exporter_item->getSetting('dateFormat')) {
         $va_get_options['dateFormat'] = $vs_date_format;
     }
     // context was switched to attribute
     if ($vn_attribute_id) {
         $o_log->logInfo(_t("Processing mapping in attribute mode for attribute_id = %1.", $vn_attribute_id));
         if ($vs_source) {
             // trying to find the source only makes sense if the source is set
             $t_attr = new ca_attributes($vn_attribute_id);
             $va_values = $t_attr->getAttributeValues();
             $va_src_tmp = explode('.', $vs_source);
             if (sizeof($va_src_tmp) == 2) {
                 $o_dm = Datamodel::load();
                 if ($t_attr->get('table_num') == $o_dm->getTableNum($va_src_tmp[0])) {
                     $vs_source = $va_src_tmp[1];
                 }
             }
             $o_log->logDebug(_t("Trying to find code %1 in value array for the current attribute.", $vs_source));
             $o_log->logDebug(_t("Value array is %1.", print_r($va_values, true)));
             foreach ($va_values as $vo_val) {
                 $va_display_val_options = array();
                 if ($vo_val instanceof ListAttributeValue) {
                     // figure out list_id -- without it we can't pull display values
                     $t_element = new ca_metadata_elements($vo_val->getElementID());
                     $va_display_val_options = array('list_id' => $t_element->get('list_id'));
                     if ($t_exporter_item->getSetting('returnIdno') || $t_exporter_item->getSetting('convertCodesToIdno')) {
                         $va_display_val_options['output'] = 'idno';
                     } elseif ($t_exporter_item->getSetting('convertCodesToDisplayText')) {
                         $va_display_val_options['output'] = 'text';
                     }
                 }
                 $o_log->logDebug(_t("Trying to match code from array %1 and the code we're looking for %2.", $vo_val->getElementCode(), $vs_source));
                 if ($vo_val->getElementCode() == $vs_source) {
                     $vs_display_value = $vo_val->getDisplayValue($va_display_val_options);
                     $o_log->logDebug(_t("Found value %1.", $vs_display_value));
                     $va_item_info[] = array('text' => $vs_display_value, 'element' => $vs_element);
                 }
             }
         } else {
             // no source in attribute context probably means this is some form of wrapper, e.g. a MARC field
             $va_item_info[] = array('element' => $vs_element);
         }
     } else {
         if ($vs_source) {
             $o_log->logDebug(_t("Source for current mapping is %1", $vs_source));
             $va_matches = array();
             // CONSTANT value
             if (preg_match("/^_CONSTANT_:(.*)\$/", $vs_source, $va_matches)) {
                 $o_log->logDebug(_t("This is a constant. Value for this mapping is '%1'", trim($va_matches[1])));
                 $va_item_info[] = array('text' => trim($va_matches[1]), 'element' => $vs_element);
             } else {
                 if (in_array($vs_source, array("relationship_type_id", "relationship_type_code", "relationship_typename"))) {
                     if (isset($pa_options[$vs_source]) && strlen($pa_options[$vs_source]) > 0) {
                         $o_log->logDebug(_t("Source refers to releationship type information. Value for this mapping is '%1'", $pa_options[$vs_source]));
                         $va_item_info[] = array('text' => $pa_options[$vs_source], 'element' => $vs_element);
                     }
                 } else {
                     if (!$vb_repeat) {
                         $vs_get = $t_instance->get($vs_source, $va_get_options);
                         $o_log->logDebug(_t("Source is a simple get() for some bundle. Value for this mapping is '%1'", $vs_get));
                         $o_log->logDebug(_t("get() options are: %1", print_r($va_get_options, true)));
                         $va_item_info[] = array('text' => $vs_get, 'element' => $vs_element);
                     } else {
                         // user wants current element repeated in case of multiple returned values
                         $va_get_options['delimiter'] = ';#;';
                         $vs_values = $t_instance->get($vs_source, $va_get_options);
                         $o_log->logDebug(_t("Source is a get() that should be repeated for multiple values. Value for this mapping is '%1'. It includes the custom delimiter ';#;' that is later used to split the value into multiple values.", $vs_values));
                         $o_log->logDebug(_t("get() options are: %1", print_r($va_get_options, true)));
                         $va_tmp = explode(";#;", $vs_values);
                         foreach ($va_tmp as $vs_text) {
                             $va_item_info[] = array('element' => $vs_element, 'text' => $vs_text);
                         }
                     }
                 }
             }
         } else {
             if ($vs_template) {
                 // templates without source are probably just static text, but you never know
                 // -> run them through processor anyways
                 $vs_proc_template = caProcessTemplateForIDs($vs_template, $pn_table_num, array($pn_record_id), array());
                 $o_log->logDebug(_t("Current mapping has no source but a template '%1'. Value from extracted via template processor is '%2'", $vs_template, $vs_proc_template));
                 $va_item_info[] = array('element' => $vs_element, 'text' => $vs_proc_template);
             } else {
                 // no source, no template -> probably wrapper
                 $o_log->logDebug(_t("Current mapping has no source and no template and is probably an XML/MARC wrapper element"));
                 $va_item_info[] = array('element' => $vs_element);
             }
         }
     }
     // reset UI locale if we unset it
     if ($vs_locale) {
         $g_ui_locale = $vs_old_ui_locale;
     }
     $o_log->logDebug(_t("We're now processing other settings like default, prefix, suffix, skipIfExpression, filterByRegExp, maxLength, plugins and replacements for this mapping"));
     $o_log->logDebug(_t("Local data before processing is: %1", print_r($va_item_info, true)));
     // handle other settings and plugin hooks
     $vs_default = $t_exporter_item->getSetting('default');
     $vs_prefix = $t_exporter_item->getSetting('prefix');
     $vs_suffix = $t_exporter_item->getSetting('suffix');
     //$vs_regexp = $t_exporter_item->getSetting('filterByRegExp');		// Deprecated -- remove?
     $vn_max_length = $t_exporter_item->getSetting('maxLength');
     $vs_skip_if_expr = $t_exporter_item->getSetting('skipIfExpression');
     $vs_original_values = $t_exporter_item->getSetting('original_values');
     $vs_replacement_values = $t_exporter_item->getSetting('replacement_values');
     $va_replacements = ca_data_exporter_items::getReplacementArray($vs_original_values, $vs_replacement_values);
     foreach ($va_item_info as $vn_key => &$va_item) {
         $this->opo_app_plugin_manager->hookExportItemBeforeSettings(array('instance' => $t_instance, 'exporter_item_instance' => $t_exporter_item, 'export_item' => &$va_item));
         // handle dontReturnValueIfOnSameDayAsStart
         if (caGetOption('dontReturnValueIfOnSameDayAsStart', $va_get_options, false)) {
             if (strlen($va_item['text']) < 1) {
                 unset($va_item_info[$vn_key]);
             }
         }
         // handle skipIfExpression setting
         if ($vs_skip_if_expr) {
             // Add current value as variable "value", accessible in expressions as ^value
             $va_vars = array_merge(array('value' => $va_item['text']), ca_data_exporters::$s_variables);
             if (ExpressionParser::evaluate($vs_skip_if_expr, $va_vars)) {
                 unset($va_item_info[$vn_key]);
                 continue;
             }
         }
         // filter by regex (deprecated since you can do the same thing and more with skipIfExpression) -- remove?
         //if((strlen($va_item['text'])>0) && $vs_regexp) {
         //	if(!preg_match("!".$vs_regexp."!i", $va_item['text'])) {
         //		unset($va_item_info[$vn_key]);
         //		continue;
         //	}
         //}
         // do replacements
         $va_item['text'] = ca_data_exporter_items::replaceText($va_item['text'], $va_replacements);
         // if text is empty, fill in default
         // if text isn't empty, respect prefix and suffix
         if (strlen($va_item['text']) == 0) {
             if ($vs_default) {
                 $va_item['text'] = $vs_default;
             }
         } else {
             if (strlen($vs_prefix) > 0 || strlen($vs_suffix) > 0) {
                 $va_item['text'] = $vs_prefix . $va_item['text'] . $vs_suffix;
             }
         }
         if ($vn_max_length && strlen($va_item['text']) > $vn_max_length) {
             $va_item['text'] = substr($va_item['text'], 0, $vn_max_length) . " ...";
         }
         // if this is a variable, set the value and delete it from the export tree
         $va_matches = array();
         if (preg_match("/^_VARIABLE_:(.*)\$/", $va_item['element'], $va_matches)) {
             ca_data_exporters::$s_variables[$va_matches[1]] = $va_item['text'];
             unset($va_item_info[$vn_key]);
             continue;
         }
         // if returned value is null then we skip the item
         $this->opo_app_plugin_manager->hookExportItem(array('instance' => $t_instance, 'exporter_item_instance' => $t_exporter_item, 'export_item' => &$va_item));
     }
     $o_log->logInfo(_t("Extracted data for this mapping is as follows:"));
     foreach ($va_item_info as $va_tmp) {
         $o_log->logInfo(sprintf("    element:%-20s value: %-10s", $va_tmp['element'], $va_tmp['text']));
     }
     $va_children = $t_exporter_item->getHierarchyChildren();
     if (is_array($va_children) && sizeof($va_children) > 0) {
         $o_log->logInfo(_t("Now proceeding to process %1 direct children in the mapping hierarchy", sizeof($va_children)));
         foreach ($va_children as $va_child) {
             foreach ($va_item_info as &$va_info) {
                 $va_child_export = $this->processExporterItem($va_child['item_id'], $pn_table_num, $pn_record_id, $pa_options);
                 $va_info['children'] = array_merge((array) $va_info['children'], $va_child_export);
             }
         }
     }
     return $va_item_info;
 }
 public function editAttribute($pn_attribute_id, $pm_element_code_or_id, $pa_values, $ps_error_source = null, $pa_options = null)
 {
     $t_attr = new ca_attributes($pn_attribute_id);
     $t_attr->purify($this->purify());
     if (!$t_attr->getPrimaryKey()) {
         return false;
     }
     $vn_attr_element_id = $t_attr->get('element_id');
     $va_attr_values = $t_attr->getAttributeValues();
     if (sizeof($va_attr_values) != sizeof($pa_values) - 1) {
         // -1 to remove locale_id which is not in attribute values array
         // Value arrays are different sizes - probably means the elements in the set have been reconfigured (sub-elements added or removed)
         // so we need to force a save.
         $this->_FIELD_VALUE_CHANGED['_ca_attribute_' . $vn_attr_element_id] = true;
     } else {
         // Have any of the values changed?
         foreach ($va_attr_values as $o_attr_value) {
             $vn_element_id = $o_attr_value->getElementID();
             $vs_element_code = $this->_getElementCode($vn_element_id);
             if (isset($pa_values[$vn_element_id]) && $pa_values[$vn_element_id] !== $o_attr_value->getDisplayValue() && !($pa_values[$vn_element_id] == "" && is_null($o_attr_value->getDisplayValue())) || isset($pa_values[$vs_element_code]) && $pa_values[$vs_element_code] !== $o_attr_value->getDisplayValue() && !($pa_values[$vs_element_code] == "" && is_null($o_attr_value->getDisplayValue()))) {
                 $this->_FIELD_VALUE_CHANGED['_ca_attribute_' . $vn_attr_element_id] = true;
                 break;
             }
         }
     }
     if (isset($this->_FIELD_VALUE_CHANGED['_ca_attribute_' . $vn_attr_element_id]) && $this->_FIELD_VALUE_CHANGED['_ca_attribute_' . $vn_attr_element_id]) {
         $this->opa_attributes_to_edit[] = array('values' => $pa_values, 'attribute_id' => $pn_attribute_id, 'element' => $pm_element_code_or_id, 'error_source' => $ps_error_source . '/' . $pn_attribute_id, 'options' => $pa_options);
     }
 }