123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378 |
- <?php
- /**
- * @file
- * Provides the Class for Feeds JSONPath Parser.
- */
- /**
- * Base class for the HTML and XML parsers.
- */
- class FeedsJSONPathParser extends FeedsParser {
- protected $debug;
- /**
- * Implements FeedsParser::parse().
- */
- public function parse(FeedsSource $source, FeedsFetcherResult $fetcher_result) {
- $mappings = $this->getOwnMappings();
- $source_config = $source->getConfigFor($this);
- // Allow config inheritance.
- if (empty($source_config)) {
- $source_config = $this->config;
- }
- $this->debug = array_keys(array_filter($source_config['debug']['options']));
- $raw = trim($fetcher_result->getRaw());
- $result = new FeedsParserResult();
- // Set link so we can set the result link attribute.
- $fetcher_config = $source->getConfigFor($source->importer->fetcher);
- $result->link = $fetcher_config['source'];
- $array = json_decode(utf8_encode($raw), TRUE);
- // Support JSON lines format.
- if (!is_array($array)) {
- $raw = preg_replace('/}\s*{/', '},{', $raw);
- $raw = '[' . $raw . ']';
- $array = json_decode(utf8_encode($raw), TRUE);
- }
- unset($raw);
- if (!is_array($array)) {
- throw new Exception(t('There was an error decoding the JSON document.'));
- }
- $files = glob(dirname(__FILE__) . '/jsonpath*.php');
- require_once reset($files);
- $all_items = $this->jsonPath($array, $source_config['context']);
- unset($array);
- $this->debug($all_items, 'context');
- foreach ($all_items as $item) {
- $parsed_item = $variables = array();
- foreach ($source_config['sources'] as $source => $query) {
- // Variable substitution.
- $query = strtr($query, $variables);
- $parsed = $this->parseSourceElement($item, $query, $source);
- // Avoid null values.
- if (isset($parsed)) {
- // Variable sunstitution can't handle arrays.
- if (!is_array($parsed)) {
- $variables['{' . $mappings[$source] . '}'] = $parsed;
- }
- else {
- $variables['{' . $mappings[$source] . '}'] = '';
- }
- $parsed_item[$source] = $parsed;
- }
- }
- if (!empty($parsed_item)) {
- $result->items[] = $parsed_item;
- }
- }
- return $result;
- }
- /**
- * Utilizes the jsonPath function from jsonpath-0.8.1.php.
- *
- * jsonPath returns false if the expression returns zero results and that will
- * mess up our for loops, so return an empty array instead.
- *
- * @todo
- * Firgure out error handling.
- *
- * @param array $array
- * The input array to parse.
- * @param string $expression
- * The JSONPath expression.
- *
- * @return array
- * Returns an array that is the output of jsonPath.
- */
- protected function jsonPath($array, $expression) {
- $result = jsonPath($array, $expression);
- return ($result === FALSE) ? array() : $result;
- }
- /**
- * Parses one item from the context array.
- *
- * @param array $item
- * An array containing one item from the context.
- * @param string $query
- * A JSONPath query.
- * @param string $source
- * The source element that corresponds to the query.
- *
- * @return array
- * An array containing the results of the query.
- */
- protected function parseSourceElement($item, $query, $source) {
- if (empty($query)) {
- return;
- }
- $results = $this->jsonPath($item, $query);
- $this->debug($results, $source);
- unset($item);
- // If there is one result, return it directly. If there are no results,
- // return. Otherwise return the results.
- if (count($results) === 1) {
- return $results[0];
- }
- if (count($results) === 0) {
- return;
- }
- return $results;
- }
- /**
- * Source form.
- */
- public function sourceForm($source_config) {
- $form = array();
- if (empty($source_config)) {
- $source_config = $this->config;
- }
- if (isset($source_config['allow_override']) &&
- !$source_config['allow_override'] &&
- empty($source_config['config'])) {
- return;
- }
- // Add extensions that might get importerd.
- $fetcher = feeds_importer($this->id)->fetcher;
- if (isset($fetcher->config['allowed_extensions'])) {
- if (strpos($fetcher->config['allowed_extensions'], 'json') === FALSE) {
- $fetcher->config['allowed_extensions'] .= ' json';
- }
- }
- $mappings_ = feeds_importer($this->id)->processor->config['mappings'];
- $uniques = $mappings = array();
- foreach ($mappings_ as $mapping) {
- if (strpos($mapping['source'], 'jsonpath_parser:') === 0) {
- $mappings[$mapping['source']] = $mapping['target'];
- if ($mapping['unique']) {
- $uniques[] = $mapping['target'];
- }
- }
- }
- $form['jsonpath'] = array(
- '#type' => 'fieldset',
- '#title' => t('JSONPath Parser Settings'),
- '#collapsible' => TRUE,
- '#collapsed' => TRUE,
- '#tree' => TRUE,
- );
- if (empty($mappings)) {
- // Detect if Feeds menu structure has changed. This will take a while to
- // be released, but since I run dev it needs to work.
- $feeds_menu = feeds_ui_menu();
- if (isset($feeds_menu['admin/structure/feeds/list'])) {
- $feeds_base = 'admin/structure/feeds/edit/';
- }
- else {
- $feeds_base = 'admin/structure/feeds/';
- }
- $form['jsonpath']['error_message']['#markup'] = '<div class="help">' . t('No JSONPath mappings are defined. Define mappings !link.', array('!link' => l(t('here'), $feeds_base . $this->id . '/mapping'))) . '</div><br />';
- return $form;
- }
- $form['jsonpath']['context'] = array(
- '#type' => 'textfield',
- '#title' => t('Context'),
- '#required' => TRUE,
- '#description' => t('This is the base query, all other queries will execute in this context.'),
- '#default_value' => isset($source_config['context']) ? $source_config['context'] : '',
- '#maxlength' => 1024,
- '#size' => 80,
- );
- $form['jsonpath']['sources'] = array(
- '#type' => 'fieldset',
- );
- if (!empty($uniques)) {
- $items = array(
- format_plural(count($uniques),
- t('Field <strong>!column</strong> is mandatory and considered unique: only one item per !column value will be created.',
- array('!column' => implode(', ', $uniques))),
- t('Fields <strong>!columns</strong> are mandatory and values in these columns are considered unique: only one entry per value in one of these columns will be created.',
- array('!columns' => implode(', ', $uniques)))),
- );
- $form['jsonpath']['sources']['help']['#markup'] = '<div class="help">' . theme('item_list', array('items' => $items)) . '</div>';
- }
- $variables = array();
- foreach ($mappings as $source => $target) {
- $form['jsonpath']['sources'][$source] = array(
- '#type' => 'textfield',
- '#title' => $target,
- '#description' => t('The JSONPath expression to execute.'),
- '#default_value' => isset($source_config['sources'][$source]) ? $source_config['sources'][$source] : '',
- '#maxlength' => 1024,
- '#size' => 80,
- );
- if (!empty($variables)) {
- $variable_text = format_plural(count($variables),
- t('The variable %v is availliable for replacement.', array('%v' => implode(', ', $variables))),
- t('The variables %v are availliable for replacement.', array('%v' => implode(', ', $variables)))
- );
- $form['jsonpath']['sources'][$source]['#description'] .= '<br />' . $variable_text;
- }
- $variables[] = '{' . $target . '}';
- }
- $form['jsonpath']['debug'] = array(
- '#type' => 'fieldset',
- '#title' => t('Debug'),
- '#collapsible' => TRUE,
- '#collapsed' => TRUE,
- );
- $form['jsonpath']['debug']['options'] = array(
- '#type' => 'checkboxes',
- '#title' => t('Debug query'),
- '#options' => array_merge(array('context' => 'context'), $mappings),
- '#default_value' => isset($source_config['debug']['options']) ? $source_config['debug']['options'] : array(),
- );
- return $form;
- }
- /**
- * Override parent::configForm().
- */
- public function configForm(&$form_state) {
- $config = $this->getConfig();
- $config['config'] = TRUE;
- $form = $this->sourceForm($config);
- $form['jsonpath']['context']['#required'] = FALSE;
- $form['jsonpath']['#collapsed'] = FALSE;
- $form['jsonpath']['allow_override'] = array(
- '#type' => 'checkbox',
- '#title' => t('Allow source configuration override'),
- '#description' => t('This setting allows feed nodes to specify their own JSONPath values for the context and sources.'),
- '#default_value' => $config['allow_override'],
- );
- return $form;
- }
- /**
- * Override parent::getMappingSources().
- */
- public function getMappingSources() {
- $mappings = $this->filterMappings(feeds_importer($this->id)->processor->config['mappings']);
- $next = 0;
- if (!empty($mappings)) {
- $keys = array_keys($mappings);
- $nums = array();
- foreach ($keys as $key) {
- list(, $num) = explode(':', $key);
- $nums[] = $num;
- }
- $max = max($nums);
- $next = ++$max;
- }
- return array(
- 'jsonpath_parser:' . $next => array(
- 'name' => t('JSONPath Expression'),
- 'description' => t('Allows you to configure a JSONPath expression that will populate this field.'),
- ),
- ) + parent::getMappingSources();
- }
- public function sourceDefaults() {
- return array();
- }
- /**
- * Define defaults.
- */
- public function configDefaults() {
- return array(
- 'context' => '',
- 'sources' => array(),
- 'debug' => array(),
- 'allow_override' => FALSE,
- );
- }
- /**
- * Override parent::sourceFormValidate().
- *
- * If the values of this source are the same as the base config we set them to
- * blank to that the values will be inherited from the importer defaults.
- *
- * @param array $values
- * The values from the form to validate, passed by reference.
- */
- public function sourceFormValidate(&$values) {
- $config = $this->getConfig();
- $values = $values['jsonpath'];
- $allow_override = $config['allow_override'];
- unset($config['allow_override']);
- ksort($values);
- ksort($config);
- if ($values === $config || !$allow_override) {
- $values = array();
- return;
- }
- $this->configFormValidate($values);
- }
- /**
- * Override parent::sourceFormValidate().
- */
- public function configFormValidate(&$values) {
- if (isset($values['jsonpath'])) {
- $values = $values['jsonpath'];
- }
- $values['context'] = trim($values['context']);
- foreach ($values['sources'] as &$source) {
- $source = trim($source);
- }
- }
- /**
- * Gets the mappings that belong to this parser.
- *
- * @return array
- * An array of mappings keyed source => target.
- */
- protected function getOwnMappings() {
- $importer_config = feeds_importer($this->id)->getConfig();
- return $this->filterMappings($importer_config['processor']['config']['mappings']);
- }
- /**
- * Filters mappings, returning the ones that belong to us.
- *
- * @param array $mappings
- * A mapping array from a processor.
- *
- * @return array
- * An array of mappings keyed source => target.
- */
- protected function filterMappings($mappings) {
- $our_mappings = array();
- foreach ($mappings as $mapping) {
- if (strpos($mapping['source'], 'jsonpath_parser:') === 0) {
- $our_mappings[$mapping['source']] = $mapping['target'];
- }
- }
- return $our_mappings;
- }
- protected function debug($item, $source) {
- if (in_array($source, $this->debug)) {
- $o = '<ul>';
- foreach ($item as $i) {
- $o .= '<li>' . check_plain(var_export($i, TRUE)) . '</li>';
- }
- $o .= '</ul>';
- drupal_set_message($source . ':' . $o);
- }
- }
- }
|