Project

General

Profile

Paste
Download (25.7 KB) Statistics
| Branch: | Revision:

root / drupal7 / sites / all / modules / feeds / plugins / FeedsProcessor.inc @ 2c8c2b87

1
<?php
2

    
3
/**
4
 * @file
5
 * Contains FeedsProcessor and related classes.
6
 */
7

    
8
// Update mode for existing items.
9
define('FEEDS_SKIP_EXISTING', 0);
10
define('FEEDS_REPLACE_EXISTING', 1);
11
define('FEEDS_UPDATE_EXISTING', 2);
12

    
13
// Default limit for creating items on a page load, not respected by all
14
// processors.
15
define('FEEDS_PROCESS_LIMIT', 50);
16

    
17
/**
18
 * Thrown if a validation fails.
19
 */
20
class FeedsValidationException extends Exception {}
21

    
22
/**
23
 * Thrown if a an access check fails.
24
 */
25
class FeedsAccessException extends Exception {}
26

    
27
/**
28
 * Abstract class, defines interface for processors.
29
 */
30
abstract class FeedsProcessor extends FeedsPlugin {
31

    
32
  /**
33
   * Implements FeedsPlugin::pluginType().
34
   */
35
  public function pluginType() {
36
    return 'processor';
37
  }
38

    
39
  /**
40
   * @defgroup entity_api_wrapper Entity API wrapper.
41
   */
42

    
43
  /**
44
   * Entity type this processor operates on.
45
   */
46
  public abstract function entityType();
47

    
48
  /**
49
   * Bundle type this processor operates on.
50
   *
51
   * Defaults to the entity type for entities that do not define bundles.
52
   *
53
   * @return string|NULL
54
   *   The bundle type this processor operates on, or NULL if it is undefined.
55
   */
56
  public function bundle() {
57
    return $this->config['bundle'];
58
  }
59

    
60
  /**
61
   * Provides a list of bundle options for use in select lists.
62
   *
63
   * @return array
64
   *   A keyed array of bundle => label.
65
   */
66
  public function bundleOptions() {
67
    $options = array();
68
    foreach (field_info_bundles($this->entityType()) as $bundle => $info) {
69
      if (!empty($info['label'])) {
70
        $options[$bundle] = $info['label'];
71
      }
72
      else {
73
        $options[$bundle] = $bundle;
74
      }
75
    }
76
    return $options;
77
  }
78

    
79
  /**
80
   * Create a new entity.
81
   *
82
   * @param $source
83
   *   The feeds source that spawns this entity.
84
   *
85
   * @return
86
   *   A new entity object.
87
   */
88
  protected abstract function newEntity(FeedsSource $source);
89

    
90
  /**
91
   * Load an existing entity.
92
   *
93
   * @param $source
94
   *   The feeds source that spawns this entity.
95
   * @param $entity_id
96
   *   The unique id of the entity that should be loaded.
97
   *
98
   * @return
99
   *   A new entity object.
100
   *
101
   * @todo We should be able to batch load these, if we found all of the
102
   *   existing ids first.
103
   */
104
  protected function entityLoad(FeedsSource $source, $entity_id) {
105
    if ($this->config['update_existing'] == FEEDS_UPDATE_EXISTING) {
106
      $entities = entity_load($this->entityType(), array($entity_id));
107
      return reset($entities);
108
    }
109

    
110
    $info = $this->entityInfo();
111

    
112
    $args = array(':entity_id' => $entity_id);
113

    
114
    $table = db_escape_table($info['base table']);
115
    $key = db_escape_field($info['entity keys']['id']);
116

    
117
    return db_query("SELECT * FROM {" . $table . "} WHERE $key = :entity_id", $args)->fetchObject();
118
  }
119

    
120
  /**
121
   * Validate an entity.
122
   *
123
   * @throws FeedsValidationException $e
124
   *   If validation fails.
125
   */
126
  protected function entityValidate($entity) {}
127

    
128
  /**
129
   * Access check for saving an enity.
130
   *
131
   * @param $entity
132
   *   Entity to be saved.
133
   *
134
   * @throws FeedsAccessException $e
135
   *   If the access check fails.
136
   */
137
  protected function entitySaveAccess($entity) {}
138

    
139
  /**
140
   * Save an entity.
141
   *
142
   * @param $entity
143
   *   Entity to be saved.
144
   */
145
  protected abstract function entitySave($entity);
146

    
147
  /**
148
   * Delete a series of entities.
149
   *
150
   * @param $entity_ids
151
   *   Array of unique identity ids to be deleted.
152
   */
153
  protected abstract function entityDeleteMultiple($entity_ids);
154

    
155
  /**
156
   * Wrap entity_get_info() into a method so that extending classes can override
157
   * it and more entity information. Allowed additional keys:
158
   *
159
   * 'label plural' ... the plural label of an entity type.
160
   */
161
  protected function entityInfo() {
162
    return entity_get_info($this->entityType());
163
  }
164

    
165
  /**
166
   * @}
167
   */
168

    
169
  /**
170
   * Process the result of the parsing stage.
171
   *
172
   * @param FeedsSource $source
173
   *   Source information about this import.
174
   * @param FeedsParserResult $parser_result
175
   *   The result of the parsing stage.
176
   */
177
  public function process(FeedsSource $source, FeedsParserResult $parser_result) {
178
    $state = $source->state(FEEDS_PROCESS);
179

    
180
    while ($item = $parser_result->shiftItem()) {
181

    
182
      // Check if this item already exists.
183
      $entity_id = $this->existingEntityId($source, $parser_result);
184
      $skip_existing = $this->config['update_existing'] == FEEDS_SKIP_EXISTING;
185

    
186
      module_invoke_all('feeds_before_update', $source, $item, $entity_id);
187

    
188
      // If it exists, and we are not updating, pass onto the next item.
189
      if ($entity_id && $skip_existing) {
190
        continue;
191
      }
192

    
193
      $hash = $this->hash($item);
194
      $changed = ($hash !== $this->getHash($entity_id));
195
      $force_update = $this->config['skip_hash_check'];
196

    
197
      // Do not proceed if the item exists, has not changed, and we're not
198
      // forcing the update.
199
      if ($entity_id && !$changed && !$force_update) {
200
        continue;
201
      }
202

    
203
      try {
204

    
205
        // Load an existing entity.
206
        if ($entity_id) {
207
          $entity = $this->entityLoad($source, $entity_id);
208

    
209
          // The feeds_item table is always updated with the info for the most
210
          // recently processed entity. The only carryover is the entity_id.
211
          $this->newItemInfo($entity, $source->feed_nid, $hash);
212
          $entity->feeds_item->entity_id = $entity_id;
213
          $entity->feeds_item->is_new = FALSE;
214
        }
215

    
216
        // Build a new entity.
217
        else {
218
          $entity = $this->newEntity($source);
219
          $this->newItemInfo($entity, $source->feed_nid, $hash);
220
        }
221

    
222
        // Set property and field values.
223
        $this->map($source, $parser_result, $entity);
224
        $this->entityValidate($entity);
225

    
226
        // Allow modules to alter the entity before saving.
227
        module_invoke_all('feeds_presave', $source, $entity, $item, $entity_id);
228
        if (module_exists('rules')) {
229
          rules_invoke_event('feeds_import_'. $source->importer()->id, $entity);
230
        }
231

    
232
        // Enable modules to skip saving at all.
233
        if (!empty($entity->feeds_item->skip)) {
234
          continue;
235
        }
236

    
237
        // This will throw an exception on failure.
238
        $this->entitySaveAccess($entity);
239
        $this->entitySave($entity);
240

    
241
        // Allow modules to perform operations using the saved entity data.
242
        // $entity contains the updated entity after saving.
243
        module_invoke_all('feeds_after_save', $source, $entity, $item, $entity_id);
244

    
245
        // Track progress.
246
        if (empty($entity_id)) {
247
          $state->created++;
248
        }
249
        else {
250
          $state->updated++;
251
        }
252
      }
253

    
254
      // Something bad happened, log it.
255
      catch (Exception $e) {
256
        $state->failed++;
257
        drupal_set_message($e->getMessage(), 'warning');
258
        $message = $this->createLogMessage($e, $entity, $item);
259
        $source->log('import', $message, array(), WATCHDOG_ERROR);
260
      }
261
    }
262

    
263
    // Set messages if we're done.
264
    if ($source->progressImporting() != FEEDS_BATCH_COMPLETE) {
265
      return;
266
    }
267
    $info = $this->entityInfo();
268
    $tokens = array(
269
      '@entity' => strtolower($info['label']),
270
      '@entities' => strtolower($info['label plural']),
271
    );
272
    $messages = array();
273
    if ($state->created) {
274
      $messages[] = array(
275
       'message' => format_plural(
276
          $state->created,
277
          'Created @number @entity.',
278
          'Created @number @entities.',
279
          array('@number' => $state->created) + $tokens
280
        ),
281
      );
282
    }
283
    if ($state->updated) {
284
      $messages[] = array(
285
       'message' => format_plural(
286
          $state->updated,
287
          'Updated @number @entity.',
288
          'Updated @number @entities.',
289
          array('@number' => $state->updated) + $tokens
290
        ),
291
      );
292
    }
293
    if ($state->failed) {
294
      $messages[] = array(
295
       'message' => format_plural(
296
          $state->failed,
297
          'Failed importing @number @entity.',
298
          'Failed importing @number @entities.',
299
          array('@number' => $state->failed) + $tokens
300
        ),
301
        'level' => WATCHDOG_ERROR,
302
      );
303
    }
304
    if (empty($messages)) {
305
      $messages[] = array(
306
        'message' => t('There are no new @entities.', array('@entities' => strtolower($info['label plural']))),
307
      );
308
    }
309
    foreach ($messages as $message) {
310
      drupal_set_message($message['message']);
311
      $source->log('import', $message['message'], array(), isset($message['level']) ? $message['level'] : WATCHDOG_INFO);
312
    }
313
  }
314

    
315
  /**
316
   * Remove all stored results or stored results up to a certain time for a
317
   * source.
318
   *
319
   * @param FeedsSource $source
320
   *   Source information for this expiry. Implementers should only delete items
321
   *   pertaining to this source. The preferred way of determining whether an
322
   *   item pertains to a certain souce is by using $source->feed_nid. It is the
323
   *   processor's responsibility to store the feed_nid of an imported item in
324
   *   the processing stage.
325
   */
326
  public function clear(FeedsSource $source) {
327
    $state = $source->state(FEEDS_PROCESS_CLEAR);
328

    
329
    // Build base select statement.
330
    $info = $this->entityInfo();
331
    $select = db_select($info['base table'], 'e');
332
    $select->addField('e', $info['entity keys']['id'], 'entity_id');
333
    $select->join(
334
      'feeds_item',
335
      'fi',
336
      "e.{$info['entity keys']['id']} = fi.entity_id AND fi.entity_type = '{$this->entityType()}'");
337
    $select->condition('fi.id', $this->id);
338
    $select->condition('fi.feed_nid', $source->feed_nid);
339

    
340
    // If there is no total, query it.
341
    if (!$state->total) {
342
      $state->total = $select->countQuery()
343
        ->execute()
344
        ->fetchField();
345
    }
346

    
347
    // Delete a batch of entities.
348
    $entities = $select->range(0, $this->getLimit())->execute();
349
    $entity_ids = array();
350
    foreach ($entities as $entity) {
351
      $entity_ids[$entity->entity_id] = $entity->entity_id;
352
    }
353
    $this->entityDeleteMultiple($entity_ids);
354

    
355
    // Report progress, take into account that we may not have deleted as
356
    // many items as we have counted at first.
357
    if (count($entity_ids)) {
358
      $state->deleted += count($entity_ids);
359
      $state->progress($state->total, $state->deleted);
360
    }
361
    else {
362
      $state->progress($state->total, $state->total);
363
    }
364

    
365
    // Report results when done.
366
    if ($source->progressClearing() == FEEDS_BATCH_COMPLETE) {
367
      if ($state->deleted) {
368
        $message = format_plural(
369
          $state->deleted,
370
          'Deleted @number @entity',
371
          'Deleted @number @entities',
372
          array(
373
            '@number' => $state->deleted,
374
            '@entity' => strtolower($info['label']),
375
            '@entities' => strtolower($info['label plural']),
376
          )
377
        );
378
        $source->log('clear', $message, array(), WATCHDOG_INFO);
379
        drupal_set_message($message);
380
      }
381
      else {
382
        drupal_set_message(t('There are no @entities to be deleted.', array('@entities' => $info['label plural'])));
383
      }
384
    }
385
  }
386

    
387
  /*
388
   * Report number of items that can be processed per call.
389
   *
390
   * 0 means 'unlimited'.
391
   *
392
   * If a number other than 0 is given, Feeds parsers that support batching
393
   * will only deliver this limit to the processor.
394
   *
395
   * @see FeedsSource::getLimit()
396
   * @see FeedsCSVParser::parse()
397
   */
398
  public function getLimit() {
399
    return variable_get('feeds_process_limit', FEEDS_PROCESS_LIMIT);
400
  }
401

    
402
  /**
403
   * Delete feed items younger than now - $time. Do not invoke expire on a
404
   * processor directly, but use FeedsImporter::expire() instead.
405
   *
406
   * @see FeedsImporter::expire().
407
   * @see FeedsDataProcessor::expire().
408
   *
409
   * @param $time
410
   *   If implemented, all items produced by this configuration that are older
411
   *   than REQUEST_TIME - $time should be deleted.
412
   *   If $time === NULL processor should use internal configuration.
413
   *
414
   * @return
415
   *   FEEDS_BATCH_COMPLETE if all items have been processed, a float between 0
416
   *   and 0.99* indicating progress otherwise.
417
   */
418
  public function expire($time = NULL) {
419
    return FEEDS_BATCH_COMPLETE;
420
  }
421

    
422
  /**
423
   * Counts the number of items imported by this processor.
424
   */
425
  public function itemCount(FeedsSource $source) {
426
    return db_query("SELECT count(*) FROM {feeds_item} WHERE id = :id AND entity_type = :entity_type AND feed_nid = :feed_nid", array(':id' => $this->id, ':entity_type' => $this->entityType(), ':feed_nid' => $source->feed_nid))->fetchField();
427
  }
428

    
429
  /**
430
   * Execute mapping on an item.
431
   *
432
   * This method encapsulates the central mapping functionality. When an item is
433
   * processed, it is passed through map() where the properties of $source_item
434
   * are mapped onto $target_item following the processor's mapping
435
   * configuration.
436
   *
437
   * For each mapping FeedsParser::getSourceElement() is executed to retrieve
438
   * the source element, then FeedsProcessor::setTargetElement() is invoked
439
   * to populate the target item properly. Alternatively a
440
   * hook_x_targets_alter() may have specified a callback for a mapping target
441
   * in which case the callback is asked to populate the target item instead of
442
   * FeedsProcessor::setTargetElement().
443
   *
444
   * @ingroup mappingapi
445
   *
446
   * @see hook_feeds_parser_sources_alter()
447
   * @see hook_feeds_data_processor_targets_alter()
448
   * @see hook_feeds_node_processor_targets_alter()
449
   * @see hook_feeds_term_processor_targets_alter()
450
   * @see hook_feeds_user_processor_targets_alter()
451
   */
452
  protected function map(FeedsSource $source, FeedsParserResult $result, $target_item = NULL) {
453

    
454
    // Static cache $targets as getMappingTargets() may be an expensive method.
455
    static $sources;
456
    if (!isset($sources[$this->id])) {
457
      $sources[$this->id] = feeds_importer($this->id)->parser->getMappingSources();
458
    }
459
    static $targets;
460
    if (!isset($targets[$this->id])) {
461
      $targets[$this->id] = $this->getMappingTargets();
462
    }
463
    $parser = feeds_importer($this->id)->parser;
464
    if (empty($target_item)) {
465
      $target_item = array();
466
    }
467

    
468
    // Many mappers add to existing fields rather than replacing them. Hence we
469
    // need to clear target elements of each item before mapping in case we are
470
    // mapping on a prepopulated item such as an existing node.
471
    foreach ($this->config['mappings'] as $mapping) {
472
      if (isset($targets[$this->id][$mapping['target']]['real_target'])) {
473
        unset($target_item->{$targets[$this->id][$mapping['target']]['real_target']});
474
      }
475
      elseif (isset($target_item->{$mapping['target']})) {
476
        unset($target_item->{$mapping['target']});
477
      }
478
    }
479

    
480
    /*
481
    This is where the actual mapping happens: For every mapping we envoke
482
    the parser's getSourceElement() method to retrieve the value of the source
483
    element and pass it to the processor's setTargetElement() to stick it
484
    on the right place of the target item.
485

    
486
    If the mapping specifies a callback method, use the callback instead of
487
    setTargetElement().
488
    */
489
    self::loadMappers();
490
    foreach ($this->config['mappings'] as $mapping) {
491
      // Retrieve source element's value from parser.
492
      if (isset($sources[$this->id][$mapping['source']]) &&
493
          is_array($sources[$this->id][$mapping['source']]) &&
494
          isset($sources[$this->id][$mapping['source']]['callback']) &&
495
          function_exists($sources[$this->id][$mapping['source']]['callback'])) {
496
        $callback = $sources[$this->id][$mapping['source']]['callback'];
497
        $value = $callback($source, $result, $mapping['source']);
498
      }
499
      else {
500
        $value = $parser->getSourceElement($source, $result, $mapping['source']);
501
      }
502

    
503
      // Map the source element's value to the target.
504
      if (isset($targets[$this->id][$mapping['target']]) &&
505
          is_array($targets[$this->id][$mapping['target']]) &&
506
          isset($targets[$this->id][$mapping['target']]['callback']) &&
507
          function_exists($targets[$this->id][$mapping['target']]['callback'])) {
508
        $callback = $targets[$this->id][$mapping['target']]['callback'];
509
        $callback($source, $target_item, $mapping['target'], $value, $mapping);
510
      }
511
      else {
512
        $this->setTargetElement($source, $target_item, $mapping['target'], $value, $mapping);
513
      }
514
    }
515
    return $target_item;
516
  }
517

    
518
  /**
519
   * Per default, don't support expiry. If processor supports expiry of imported
520
   * items, return the time after which items should be removed.
521
   */
522
  public function expiryTime() {
523
    return FEEDS_EXPIRE_NEVER;
524
  }
525

    
526
  /**
527
   * Declare default configuration.
528
   */
529
  public function configDefaults() {
530
    $info = $this->entityInfo();
531
    $bundle = NULL;
532
    if (empty($info['entity keys']['bundle'])) {
533
      $bundle = $this->entityType();
534
    }
535
    return array(
536
      'mappings' => array(),
537
      'update_existing' => FEEDS_SKIP_EXISTING,
538
      'input_format' => NULL,
539
      'skip_hash_check' => FALSE,
540
      'bundle' => $bundle,
541
    );
542
  }
543

    
544
  /**
545
   * Overrides parent::configForm().
546
   */
547
  public function configForm(&$form_state) {
548
    $info = $this->entityInfo();
549
    $form = array();
550

    
551
    if (!empty($info['entity keys']['bundle'])) {
552
      $form['bundle'] = array(
553
        '#type' => 'select',
554
        '#options' => $this->bundleOptions(),
555
        '#title' => !empty($info['bundle name']) ? $info['bundle name'] : t('Bundle'),
556
        '#required' => TRUE,
557
        '#default_value' => $this->bundle(),
558
      );
559
    }
560
    else {
561
      $form['bundle'] = array(
562
        '#type' => 'value',
563
        '#value' => $this->entityType(),
564
      );
565
    }
566

    
567
    $tokens = array('@entities' => strtolower($info['label plural']));
568

    
569
    $form['update_existing'] = array(
570
      '#type' => 'radios',
571
      '#title' => t('Update existing @entities', $tokens),
572
      '#description' =>
573
        t('Existing @entities will be determined using mappings that are a "unique target".', $tokens),
574
      '#options' => array(
575
        FEEDS_SKIP_EXISTING => t('Do not update existing @entities', $tokens),
576
        FEEDS_REPLACE_EXISTING => t('Replace existing @entities', $tokens),
577
        FEEDS_UPDATE_EXISTING => t('Update existing @entities', $tokens),
578
      ),
579
      '#default_value' => $this->config['update_existing'],
580
    );
581
    global $user;
582
    $formats = filter_formats($user);
583
    foreach ($formats as $format) {
584
      $format_options[$format->format] = $format->name;
585
    }
586
    $form['skip_hash_check'] = array(
587
      '#type' => 'checkbox',
588
      '#title' => t('Skip hash check'),
589
      '#description' => t('Force update of items even if item source data did not change.'),
590
      '#default_value' => $this->config['skip_hash_check'],
591
    );
592
    $form['input_format'] = array(
593
      '#type' => 'select',
594
      '#title' => t('Text format'),
595
      '#description' => t('Select the input format for the body field of the nodes to be created.'),
596
      '#options' => $format_options,
597
      '#default_value' => isset($this->config['input_format']) ? $this->config['input_format'] : 'plain_text',
598
      '#required' => TRUE,
599
    );
600

    
601
    return $form;
602
  }
603

    
604
  /**
605
   * Get mappings.
606
   */
607
  public function getMappings() {
608
    return isset($this->config['mappings']) ? $this->config['mappings'] : array();
609
  }
610

    
611
  /**
612
   * Declare possible mapping targets that this processor exposes.
613
   *
614
   * @ingroup mappingapi
615
   *
616
   * @return
617
   *   An array of mapping targets. Keys are paths to targets
618
   *   separated by ->, values are TRUE if target can be unique,
619
   *   FALSE otherwise.
620
   */
621
  public function getMappingTargets() {
622

    
623
    // The bundle has not been selected.
624
    if (!$this->bundle()) {
625
      $info = $this->entityInfo();
626
      $bundle_name = !empty($info['bundle name']) ? drupal_strtolower($info['bundle name']) : t('bundle');
627
      $plugin_key = feeds_importer($this->id)->config['processor']['plugin_key'];
628
      $url = url('admin/structure/feeds/' . $this->id . '/settings/' . $plugin_key);
629
      drupal_set_message(t('Please <a href="@url">select a @bundle_name</a>.', array('@url' => $url, '@bundle_name' => $bundle_name)), 'warning', FALSE);
630
    }
631

    
632
    return array(
633
      'url' => array(
634
        'name' => t('URL'),
635
        'description' => t('The external URL of the item. E. g. the feed item URL in the case of a syndication feed. May be unique.'),
636
        'optional_unique' => TRUE,
637
      ),
638
      'guid' => array(
639
        'name' => t('GUID'),
640
        'description' => t('The globally unique identifier of the item. E. g. the feed item GUID in the case of a syndication feed. May be unique.'),
641
        'optional_unique' => TRUE,
642
      ),
643
    );
644
  }
645

    
646
  /**
647
   * Set a concrete target element. Invoked from FeedsProcessor::map().
648
   *
649
   * @ingroup mappingapi
650
   */
651
  public function setTargetElement(FeedsSource $source, $target_item, $target_element, $value) {
652
    switch ($target_element) {
653
      case 'url':
654
      case 'guid':
655
        $target_item->feeds_item->$target_element = $value;
656
        break;
657
      default:
658
        $target_item->$target_element = $value;
659
        break;
660
    }
661
  }
662

    
663
  /**
664
   * Retrieve the target entity's existing id if available. Otherwise return 0.
665
   *
666
   * @ingroup mappingapi
667
   *
668
   * @param FeedsSource $source
669
   *   The source information about this import.
670
   * @param $result
671
   *   A FeedsParserResult object.
672
   *
673
   * @return
674
   *   The serial id of an entity if found, 0 otherwise.
675
   */
676
  protected function existingEntityId(FeedsSource $source, FeedsParserResult $result) {
677
    $query = db_select('feeds_item')
678
      ->fields('feeds_item', array('entity_id'))
679
      ->condition('feed_nid', $source->feed_nid)
680
      ->condition('entity_type', $this->entityType())
681
      ->condition('id', $source->id);
682

    
683
    // Iterate through all unique targets and test whether they do already
684
    // exist in the database.
685
    foreach ($this->uniqueTargets($source, $result) as $target => $value) {
686
      switch ($target) {
687
        case 'url':
688
          $entity_id = $query->condition('url', $value)->execute()->fetchField();
689
          break;
690
        case 'guid':
691
          $entity_id = $query->condition('guid', $value)->execute()->fetchField();
692
          break;
693
      }
694
      if (isset($entity_id)) {
695
        // Return with the content id found.
696
        return $entity_id;
697
      }
698
    }
699
    return 0;
700
  }
701

    
702

    
703
  /**
704
   * Utility function that iterates over a target array and retrieves all
705
   * sources that are unique.
706
   *
707
   * @param $batch
708
   *   A FeedsImportBatch.
709
   *
710
   * @return
711
   *   An array where the keys are target field names and the values are the
712
   *   elements from the source item mapped to these targets.
713
   */
714
  protected function uniqueTargets(FeedsSource $source, FeedsParserResult $result) {
715
    $parser = feeds_importer($this->id)->parser;
716
    $targets = array();
717
    foreach ($this->config['mappings'] as $mapping) {
718
      if (!empty($mapping['unique'])) {
719
        // Invoke the parser's getSourceElement to retrieve the value for this
720
        // mapping's source.
721
        $targets[$mapping['target']] = $parser->getSourceElement($source, $result, $mapping['source']);
722
      }
723
    }
724
    return $targets;
725
  }
726

    
727
  /**
728
   * Adds Feeds specific information on $entity->feeds_item.
729
   *
730
   * @param $entity
731
   *   The entity object to be populated with new item info.
732
   * @param $feed_nid
733
   *   The feed nid of the source that produces this entity.
734
   * @param $hash
735
   *   The fingerprint of the source item.
736
   */
737
  protected function newItemInfo($entity, $feed_nid, $hash = '') {
738
    $entity->feeds_item = new stdClass();
739
    $entity->feeds_item->is_new = TRUE;
740
    $entity->feeds_item->entity_id = 0;
741
    $entity->feeds_item->entity_type = $this->entityType();
742
    $entity->feeds_item->id = $this->id;
743
    $entity->feeds_item->feed_nid = $feed_nid;
744
    $entity->feeds_item->imported = REQUEST_TIME;
745
    $entity->feeds_item->hash = $hash;
746
    $entity->feeds_item->url = '';
747
    $entity->feeds_item->guid = '';
748
  }
749

    
750
  /**
751
   * Loads existing entity information and places it on $entity->feeds_item.
752
   *
753
   * @param $entity
754
   *   The entity object to load item info for. Id key must be present.
755
   *
756
   * @return
757
   *   TRUE if item info could be loaded, false if not.
758
   */
759
  protected function loadItemInfo($entity) {
760
    $entity_info = entity_get_info($this->entityType());
761
    $key = $entity_info['entity keys']['id'];
762
    if ($item_info = feeds_item_info_load($this->entityType(), $entity->$key)) {
763
      $entity->feeds_item = $item_info;
764
      return TRUE;
765
    }
766
    return FALSE;
767
  }
768

    
769
  /**
770
   * Create MD5 hash of item and mappings array.
771
   *
772
   * Include mappings as a change in mappings may have an affect on the item
773
   * produced.
774
   *
775
   * @return Always returns a hash, even with empty, NULL, FALSE:
776
   *  Empty arrays return 40cd750bba9870f18aada2478b24840a
777
   *  Empty/NULL/FALSE strings return d41d8cd98f00b204e9800998ecf8427e
778
   */
779
  protected function hash($item) {
780
    return hash('md5', serialize($item) . serialize($this->config['mappings']));
781
  }
782

    
783
  /**
784
   * Retrieves the MD5 hash of $entity_id from the database.
785
   *
786
   * @return string
787
   *   Empty string if no item is found, hash otherwise.
788
   */
789
  protected function getHash($entity_id) {
790

    
791
    if ($hash = db_query("SELECT hash FROM {feeds_item} WHERE entity_type = :type AND entity_id = :id", array(':type' => $this->entityType(), ':id' => $entity_id))->fetchField()) {
792
      // Return with the hash.
793
      return $hash;
794
    }
795
    return '';
796
  }
797

    
798
  /**
799
   * Creates a log message for when an exception occured during import.
800
   *
801
   * @param Exception $e
802
   *   The exception that was throwned during processing the item.
803
   * @param $entity
804
   *   The entity object.
805
   * @param $item
806
   *   The parser result for this entity.
807
   *
808
   * @return string
809
   *   The message to log.
810
   */
811
  protected function createLogMessage(Exception $e, $entity, $item) {
812
    include_once DRUPAL_ROOT . '/includes/utility.inc';
813
    $message = $e->getMessage();
814
    $message .= '<h3>Original item</h3>';
815
    $message .= '<pre>' . check_plain(drupal_var_export($item)) . '</pre>';
816
    $message .= '<h3>Entity</h3>';
817
    $message .= '<pre>' . check_plain(drupal_var_export($entity)) . '</pre>';
818
    return $message;
819
  }
820

    
821
}
822

    
823
class FeedsProcessorBundleNotDefined extends Exception {}