You are here

sarnia.module in Sarnia 7

File

sarnia.module
View source
<?php

// Field formatters.
require_once 'sarnia.field_formatter.inc';

// OpenLayers hook implementations.
require_once 'sarnia.openlayers.inc';

/**
 * Implements hook_menu().
 */
function sarnia_menu() {
  $items = array();
  $items['admin/config/search/search_api/server/%search_api_server/sarnia'] = array(
    'title' => 'Sarnia',
    'page callback' => 'drupal_get_form',
    'page arguments' => array(
      'sarnia_entity_manage_form',
      5,
    ),
    'access callback' => '_sarnia_entity_manage_form_access',
    'access arguments' => array(
      5,
    ),
    'file' => 'sarnia.entities.inc',
    'type' => MENU_LOCAL_TASK,
    'context' => MENU_CONTEXT_INLINE | MENU_CONTEXT_PAGE,
  );
  $items['admin/config/search/search_api/server/%search_api_server/sarnia/manage'] = array(
    'title' => 'Manage entity',
    'type' => MENU_DEFAULT_LOCAL_TASK,
  );
  $items['admin/config/search/search_api/server/%search_api_server/sarnia/delete'] = array(
    'title' => 'Delete',
    'page callback' => 'drupal_get_form',
    'page arguments' => array(
      'sarnia_entity_delete_form',
      5,
    ),
    'access callback' => '_sarnia_entity_manage_form_access',
    'access arguments' => array(
      5,
    ),
    'file' => 'sarnia.entities.inc',
    'type' => MENU_CALLBACK,
  );
  $items['admin/config/search/search_api/server/%search_api_server/sarnia/cache'] = array(
    'title' => 'Refresh server field cache',
    'page callback' => 'drupal_get_form',
    'page arguments' => array(
      'sarnia_entity_cache_form',
      5,
    ),
    'access arguments' => array(
      'administer site configuration',
    ),
    'file' => 'sarnia.entities.inc',
    'type' => MENU_CALLBACK,
  );
  $items['admin/config/search/search_api/server/%search_api_server/sarnia/properties'] = array(
    'title' => 'Solr properties',
    'page callback' => 'drupal_get_form',
    'page arguments' => array(
      'sarnia_entity_properties_form',
      5,
    ),
    'access arguments' => array(
      'administer site configuration',
    ),
    'file' => 'sarnia.entities.inc',
    'type' => MENU_LOCAL_TASK,
  );
  $items['admin/config/search/search_api/server/%search_api_server/sarnia/schema'] = array(
    'title' => 'Solr Schema',
    'page callback' => 'sarnia_schema_page',
    'page arguments' => array(
      5,
    ),
    'access arguments' => array(
      'administer site configuration',
    ),
    'file' => 'sarnia.rules.inc',
    'type' => MENU_LOCAL_TASK,
  );
  $items['sarnia/%sarnia_entity_type/%sarnia'] = array(
    'title' => 'A Sarnia Entity',
    'load arguments' => array(
      1,
      2,
    ),
    'page callback' => 'sarnia_entity_page',
    'page arguments' => array(
      1,
      2,
    ),
    'access arguments' => array(
      'access sarnia entity pages',
    ),
  );
  return $items;
}

/**
 * Implements hook_menu_alter().
 *
 * This totally sucks, but if we want Field UI forms to live under the "Sarnia"
 * tab of Search API server pages, this is what we have to do.
 *
 * Field UI hard-codes entity and bundle names into the menu array, or else it
 * requires that they be present in the path. Also, the Sarnia tab path plus the
 * Field UI field paths are longer than Drupal's limit of 9 menu arguments.
 *
 * Moving Sarnia bundle field management to somewhere else in the menu system
 * would mean we could remove this entire hook_menu_alter() implementation, but
 * for now it is more important to keep the UI in one place.
 */
function sarnia_menu_alter(&$items) {
  if (!module_exists('field_ui')) {
    return;
  }

  // Remove paths added by Field UI.
  foreach ($items as $path => $item) {
    if (preg_match('#^admin/config/search/search_api/server/[^/]+/sarnia/(field|display)#', $path)) {
      unset($items[$path]);
    }
  }

  // Add customized Field UI paths.
  $base = array(
    'page callback' => 'drupal_get_form',
    'access callback' => 'user_access',
    'access arguments' => array(
      'administer site configuration',
    ),
    'file' => 'field_ui.admin.inc',
    'file path' => drupal_get_path('module', 'field_ui'),
  );
  $items['admin/config/search/search_api/server/%sarnia_entity_server_name/sarnia/fields'] = array(
    'title' => 'Manage fields',
    'page arguments' => array(
      'field_ui_field_overview_form',
      5,
      5,
    ),
    'type' => MENU_LOCAL_TASK,
    'weight' => 9,
  ) + $base;

  // The field edit path has sub-paths for editing (default), updating field
  // settings, changing the widget type, and deleting the field, but the Drupal
  // menu system only goes 9 levels deep, and Sarnia bundles happen to be
  // located too deep.
  // - sarnia_field_ui_menu_access_callback() makes sure that the sub-path is
  //   allowed.
  // - sarnia_field_ui_menu_page_callback() summons the correct field editing
  //   form.
  $items['admin/config/search/search_api/server/%sarnia_entity_server_name/sarnia/fields/%sarnia_field_ui_menu'] = array(
    'load arguments' => array(
      5,
      '%map',
    ),
    'title callback' => 'field_ui_menu_title',
    'title arguments' => array(
      8,
    ),
    'access callback' => 'sarnia_field_ui_menu_access_callback',
    'access arguments' => array(
      'administer site configuration',
      9,
    ),
    'page callback' => 'sarnia_field_ui_menu_page_callback',
    'page arguments' => array(
      8,
    ),
  ) + $base;
  $items['admin/config/search/search_api/server/%sarnia_entity_server_name/sarnia/display'] = array(
    'title' => 'Manage display',
    'page arguments' => array(
      'field_ui_display_overview_form',
      5,
      5,
      'default',
    ),
    'type' => MENU_LOCAL_TASK,
    'weight' => 10,
  ) + $base;
  $items['admin/config/search/search_api/server/%sarnia_entity_server_name/sarnia/display/default'] = array(
    'title' => 'Default',
    'page arguments' => array(
      'field_ui_display_overview_form',
      5,
      5,
      8,
    ),
    'access callback' => '_field_ui_view_mode_menu_access',
    'access arguments' => array(
      5,
      5,
      8,
      'user_access',
      'administer site configuration',
    ),
    'type' => MENU_DEFAULT_LOCAL_TASK,
  ) + $base;
  foreach (sarnia_entity_types() as $type) {
    $entity_info = entity_get_info($type['machine_name']);
    $search_api_server = $type['search_api_server'];
    if (is_array($entity_info['view modes'])) {
      foreach ($entity_info['view modes'] as $view_mode => $view_mode_info) {
        $items["admin/config/search/search_api/server/{$search_api_server}/sarnia/display/{$view_mode}"] = array(
          'title' => $view_mode_info['label'],
          'page arguments' => array(
            'field_ui_display_overview_form',
            $type['machine_name'],
            $type['machine_name'],
            $view_mode,
          ),
          'access callback' => '_field_ui_view_mode_menu_access',
          'access arguments' => array(
            $type['machine_name'],
            $type['machine_name'],
            $view_mode,
            'user_access',
            'administer site configuration',
          ),
          'type' => MENU_LOCAL_TASK,
        ) + $base;
      }
    }
  }
}

/**
 * Implements hook_permission().
 */
function sarnia_permission() {
  return array(
    'administer sarnia entity types' => array(
      'title' => t('Administer Sarnia entity types'),
    ),
    'access sarnia entity pages' => array(
      'title' => t('Access sarnia entity pages'),
    ),
  );
}

/**
 * Determine whether to show the 'Sarnia' tab on a particular Search API server
 * page.
 *   - Sarnia entity types are only supported if the Search API server uses Solr.
 *   - The current user must have permission to configure Sarnia entity types.
 *
 * @see sarnia_menu()
 */
function _sarnia_entity_manage_form_access($server) {
  return $server->class == 'sarnia_solr_service' && user_access('administer sarnia entity types');
}

/**
 * Get information about all Sarnia-provided entities.
 *
 * This function loads entity information using entity_get_info(), filters on
 * the entity controller class property, and returns just the bundle
 * information.
 *
 * @return array
 *   An array whose keys are entity type names and values are entity bundle info
 *   arrays.
 */
function sarnia_entity_types($reset = FALSE) {
  $entity_types =& drupal_static(__FUNCTION__);
  if (!isset($entity_types) || $reset) {

    // Find entities that use the Sarnia controller.
    $entity_types = array();
    foreach (entity_get_info() as $type_name => $type) {
      if ($type['controller class'] == 'SarniaController') {

        // Extract the entity's bundle information.
        $entity_types[$type_name] = $type['bundles'][$type_name];
      }
    }
  }
  return $entity_types;
}

/**
 * Load bundle information about a specific Sarnia entity type.
 *
 * This function is used as a menu wildcard loader, %sarnia_entity_type.
 * @see sarnia_menu()
 *
 * @param string $machine_name
 *   The machine name of a Sarnia entity type.
 *
 * @return array
 *   Bundle information, or FALSE if there is no Sarnia entity type
 *   with the given machine name.
 */
function sarnia_entity_type_load($machine_name) {
  $entity_types = sarnia_entity_types();
  if (isset($entity_types[$machine_name])) {
    return $entity_types[$machine_name];
  }
  return FALSE;
}

/**
 * Load Sarnia entity type info given a Search API index machine name.
 *
 * This function works on the assumption that there will be a single Search API
 * index for a particular Sarnia entity type.
 *
 * @param string $index_machine_name
 *   The machine name of a Search API index.
 *
 * @return array
 *   Bundle information, or FALSE if there is no Sarnia entity type
 *   associated with the given Search API index.
 */
function sarnia_entity_type_load_by_index($index_machine_name) {
  foreach (sarnia_entity_types() as $entity_type) {
    if ($entity_type['search_api_index'] == $index_machine_name) {
      return $entity_type;
    }
  }
  return FALSE;
}

/**
 * Get a Sarnia entity type machine name given a Search API server machine name.
 *
 * The fact that this function's name ends in '_load' is deceptive, since it
 * doesn't actually load entity type information or an entity. However, it is
 * used as a wildcard menu loader for Field UI administration paths.
 * @see sarnia_entity_info().
 *
 * This function works on the assumption that there will be a single Sarnia
 * entity type for a particular Search API server.
 *
 * @param string $server_name
 *   The machine name of a Search API server.
 *
 * @return string
 *  The machine name of a Sarnia entity type, or FALSE if there is no
 *  Sarnia entity type associated with the given Search API server.
 */
function sarnia_entity_server_name_load($server_name) {
  foreach (sarnia_entity_types() as $entity_type) {
    if ($entity_type['search_api_server'] == $server_name) {
      return $entity_type['machine_name'];
    }
  }
  return FALSE;
}

/**
 * Transform the server name argument into entity type and bundle arguments for field_ui_menu_load().
 * @see sarnia_menu_alter()
 */
function sarnia_field_ui_menu_load($field_name, $server_name, $map = array()) {
  $entity_type = sarnia_entity_server_name_load($server_name);
  return field_ui_menu_load($field_name, $entity_type, $entity_type, 0, array());
}

/**
 * Check whether a Sarnia bundle Field UI sub-path is valid, and do the access
 * check.
 * @see sarnia_menu_alter()
 */
function sarnia_field_ui_menu_access_callback($perm, $path) {
  return user_access($perm) && (!$path || in_array($path, array(
    'edit',
    'field-settings',
    'widget-type',
    'delete',
  )));
}

/**
 * Load the appropriate form for a particular Sarnia bundle Field UI sub-path.
 * @see sarnia_menu_alter()
 */
function sarnia_field_ui_menu_page_callback($field, $path = 'edit') {
  switch ($path) {
    case 'field-settings':
      $form = 'field_ui_field_settings_form';
      break;
    case 'widget-type':
      $form = 'field_ui_widget_type_form';
      break;
    case 'delete':
      $form = 'field_ui_field_delete_form';
      break;
    case 'edit':
    default:
      $form = 'field_ui_field_edit_form';
  }
  return drupal_get_form($form, $field);
}

/**
 * Menu load callback; load a Sarnia entity.
 */
function sarnia_load($entity_id, $entity_type) {
  if ($entities = entity_load($entity_type, array(
    $entity_id,
  ))) {
    return $entities[$entity_id];
  }
  return FALSE;
}

/**
 * Menu page callback to display a Sarnia entity.
 *
 * Renders a Sarnia entity using the default display mode.
 *
 * The 'sarnia_id' and 'sarnia_solr_properties' items provided by Sarnia's
 * hook_field_extra_fields() implementation should be useful to developers and
 * administrators who wish to view the full contents of a particular Sarnia
 * entity.
 *
 * @see sarnia_field_extra_fields()
 */
function sarnia_entity_page($entity_type, $entity) {
  return sarnia_view($entity);
}

/**
 * Implements hook_help().
 *
 * Provide help text on the Sarnia local task under the Search API server
 * administration pages.
 */
function sarnia_help($path, $arg) {
  if ($path == 'admin/config/search/search_api/server/%/sarnia') {
    return t("Sarnia can provide an entity type based on data that already exists in this Solr core. This is most useful for Solr cores that contain non-Drupal data that is indexed by an external process, since it allows you to use data stored in Solr within Drupal. In combination with a read-only Search API index, you can also build Views based on the Solr data.");
  }
  if ($path == 'admin/help#sarnia') {
    $filepath = dirname(__FILE__) . '/README.md';
    if (file_exists($filepath)) {
      $readme = file_get_contents($filepath);
    }
    else {
      $filepath = dirname(__FILE__) . '/README.txt';
      if (file_exists($filepath)) {
        $readme = file_get_contents($filepath);
      }
    }
    if (!isset($readme)) {
      return NULL;
    }
    if (module_exists('markdown')) {
      $filters = module_invoke('markdown', 'filter_info');
      $info = $filters['filter_markdown'];
      if (function_exists($info['process callback'])) {
        $output = $info['process callback']($readme, NULL);
      }
      else {
        $output = '<pre>' . $readme . '</pre>';
      }
    }
    else {
      $output = '<pre>' . $readme . '</pre>';
    }
    return $output;
  }
}

/**
 * Implements hook_theme().
 *
 * Provide a template for displaying Sarnia entities.
 */
function sarnia_theme($existing, $type, $theme, $path) {
  return array(
    'sarnia_entity' => array(
      'render element' => 'elements',
      'template' => 'sarnia-entity',
    ),
    'sarnia_schema_rule_form' => array(
      'file' => 'sarnia.rules.inc',
      'render element' => 'form',
    ),
  );
}

/**
 * Implements hook_entity_info().
 *
 * Provide entity types to represent data from Solr, based on Search API
 * servers. Because of the way Search API indexes work, data from each Search
 * API server needs to be represented as an independent entity type, rather than
 * as a bundle on a single Sarnia entity type.
 *
 * In other parts of this module, entity types provided here are referred to as
 * "Sarnia entity types".
 */
function sarnia_entity_info() {
  $entities = array();

  // Load entity type configuration information from the database.
  module_load_include('inc', 'sarnia', 'sarnia.entities');
  foreach (_sarnia_entity_types() as $machine_name => $type) {
    $entities[$machine_name] = array(
      'label' => $type['label'],
      'controller class' => 'SarniaController',
      'fieldable' => TRUE,
      'static cache' => TRUE,
      'uri callback' => 'sarnia_uri',
      'view callback' => 'sarnia_view_multiple',
      'base table' => NULL,
      // Prevent undefined array index errors from Views.
      'entity keys' => array(
        'id' => 'id',
        'revision' => FALSE,
        'bundle' => FALSE,
      ),
      'bundles' => array(
        $machine_name => array_merge($type, array(
          'admin' => array(
            'path' => "admin/config/search/search_api/server/{$type['search_api_server']}/sarnia",
          ),
        )),
      ),
      'view modes' => array(),
    );
  }
  return $entities;
}

/**
 * Provide a path for a particular entity.
 *
 * @param object $entity
 *   An instance of a Sarnia entity.
 *
 * @return array
 *   Arguments to pass to url() in order to get the URI of a Sarnia entity.
 *
 * @see sarnia_entity_info()
 */
function sarnia_uri($entity) {
  return array(
    'path' => "sarnia/{$entity->type}/{$entity->id}",
  );
}

/**
 * Implements hook_entity_property_info().
 *
 * This hook is provided by the Entity API module.
 * @see sarnia_entity_load()
 */
function sarnia_entity_property_info() {
  module_load_include('inc', 'sarnia', 'sarnia.entities');
  $info = array();
  $entity_types = _sarnia_entity_types();
  foreach ($entity_types as $machine_name => $entity_info) {
    $info[$machine_name]['properties'] = array();

    // Add only fulltext fields as 'text' properties for Search API. If we
    // wanted all fields, we would use $server->getRemoteFields(), but that
    // could significantly increase the size of stored Search API index settings
    // and other caches; indexes can easily have over a hundred fields.
    // @see http://drupal.org/node/1308638
    $server = search_api_server_load($entity_info['search_api_server']);
    if ($server) {
      foreach ($server
        ->getFulltextFields() as $key => $field) {
        $info[$machine_name]['properties'][$key] = array(
          'label' => $key,
          'type' => 'text',
        );
      }

      // Add date fields.
      foreach ($server
        ->getRemoteFields() as $key => $field) {
        if ($field
          ->getType() == 'tdate') {
          $info[$machine_name]['properties'][$key] = array(
            'label' => $key,
            'type' => 'date',
          );
        }
      }

      // Organize properties alphabetically.
      ksort($info[$machine_name]['properties']);

      // Add the 'id' field first.
      unset($info[$machine_name]['properties']['id']);
      $info[$machine_name]['properties'] = array(
        'id' => array(
          'label' => t('Id'),
          'type' => 'token',
          'description' => t('An id from Solr.'),
          'required' => TRUE,
        ),
      ) + $info[$machine_name]['properties'];
    }
  }
  return $info;
}

/**
 * Controller class for Sarnia entities.
 *
 * This class extends the DrupalDefaultEntityController class, overriding the
 * 'load' method for entities based on Solr documents.
 */
class SarniaController extends DrupalDefaultEntityController {
  public function load($ids = array(), $conditions = array()) {

    // Get cached entities.
    $entities = $this
      ->cacheGet($ids, $conditions);

    // Load un-cached entities. In general, this will not need to load any
    // entities, since when Solr queries are run from Views,
    // SarniaSolrService::postQuery() has already stashed all of the loaded
    // entities.
    if ($ids === FALSE || ($load_ids = array_diff($ids, array_keys($entities)))) {

      // Load the Search API index to query.
      $index = search_api_index_load($this->entityInfo['bundles'][$this->entityType]['search_api_index']);

      // Disable facets when loading entities.
      $index
        ->server()
        ->disableFeature('search_api_facets');

      // Create a Search API query.
      $query = $index
        ->query(array(
        'parse mode' => 'direct',
        'sarnia use raw keys' => TRUE,
      ));

      // Result field containing the entity id.
      $id_field = $this->entityInfo['bundles'][$this->entityType]['id_field'];
      do {

        // Build the query string. We use search keys instead of filter queries
        // because they're cached differently by Solr. This loads items in
        // batches of 200.
        if (is_array($load_ids)) {
          $keys = array();
          $count = 0;
          foreach ($load_ids as $i => $id) {
            $keys[] = "{$id_field}:{$id}";
            unset($load_ids[$i]);
            $count++;
            if ($count >= 200) {
              break;
            }
          }
          $query
            ->keys(implode(' OR ', $keys));
        }

        // Run the query. We don't have to do anything beyond querying, since
        // SarniaSolrService::postQuery() stashes all entity results.
        // @todo check for entities that DON'T load, and cache those as FALSE.
        $query
          ->execute();
      } while (is_array($load_ids) && !empty($load_ids));

      // Retrieve entities from the cache, where ::stash() puts them after each
      // query.
      $entities = $this
        ->cacheGet($ids, $conditions);

      // Un-disable facets in case the server object is used again later.
      $index
        ->server()
        ->unDisableFeature('search_api_facets');
    }
    return $entities;
  }

  /**
   * Stash entities from a Solr response. This assumes that items in the
   * response contain all field data, and that all entities in the response
   * should be stashed.
   *
   * Called from SarniaSolrService::postQuery().
   *
   * @param array $results
   *   An array of results from Solr, keyed by entity id. Each result is an
   *   array of Solr properties.
   */
  public function stash($results) {
    $solr_document_field = '_data';
    foreach (current(field_info_instances($this->entityType)) as $field_name => $instance) {
      $field_info = field_info_field($field_name);
      if ($field_info['type'] == 'sarnia') {
        $solr_document_field = $field_name;
        break;
      }
    }
    $loaded_entities = array();
    foreach ($results as $entity_id => $result) {
      $loaded_entities[$entity_id] = (object) array(
        'type' => $this->entityType,
        $this->idKey => $entity_id,
        $solr_document_field => $result['fields'],
      );
    }

    // Load fields.
    if (!empty($loaded_entities)) {
      $this
        ->attachLoad($loaded_entities);
    }
    $this
      ->cacheSet($loaded_entities);
  }

}

/**
 * Implements hook_field_info().
 *
 * Provide a Field API field representing the contents of a Solr document.
 */
function sarnia_field_info() {
  return array(
    'sarnia' => array(
      'label' => t('Solr Document'),
      'description' => t('Properties loaded from a Solr document.'),
      'settings' => array(),
      'instance_settings' => array(),
      'default_widget' => 'sarnia_no_input',
      'default_formatter' => 'sarnia_default',
      'no_ui' => TRUE,
    ),
  );
}

/**
 * Implements hook_field_extra_fields().
 */
function sarnia_field_extra_fields() {
  $extra = array();
  foreach (sarnia_entity_types() as $name => $info) {
    $extra[$name][$name]['display'] = array(
      'sarnia_id' => array(
        'label' => t('Sarnia: Id'),
        'description' => t('The Sarnia entity id.'),
        'weight' => 99,
      ),
      'sarnia_solr_properties' => array(
        'label' => t('Sarnia: Solr properties'),
        'description' => t('The full set of Solr properties associated with this entity. Useful for debugging purposes.'),
        'weight' => 100,
      ),
    );
  }
  return $extra;
}

/**
 * Implements hook_field_load().
 *
 * In general, this hook should have nothing to do, since the SarniaController
 * tries to pre-load the field data.
 *
 * @see SarniaController::load()
 */
function sarnia_field_load($entity_type, $entities, $field, $instances, $langcode, &$items) {

  // If we didn't pre-load the field data in SarniaController::load(), copy it here.
  foreach ($entities as $id => $entity) {
    if (empty($entity->{$field['field_name']}) && isset($entity->_data)) {
      $entity->{$field['field_name']} = (array) $entity->_data;
      unset($entity->_data);
    }
  }
}

/**
 * Implements hook_field_widget_info().
 *
 * Provide a "no input" widget so that Field UI doesn't barf on the fact that
 * Sarnia fields have no widget (since they're read only).
 */
function sarnia_field_widget_info() {
  return array(
    'sarnia_no_input' => array(
      'label' => 'No widget',
      'description' => 'Do not provide an input form for this field.',
      'field types' => array(
        'sarnia',
      ),
      'settings' => array(),
    ),
  );
}

/**
 * Implements hook_field_widget_form().
 *
 * Returns an empty array, which is the 'sarnia_no_input' widget form.
 * @see sarnia_field_widget_info()
 */
function sarnia_field_widget_form(&$form, &$form_state, $field, $instance, $langcode, $items, $delta, $element) {
  return array();
}

/**
 * Get the value of a Solr property from a 'sarnia' Solr document field.
 *
 * @param object $entity
 *   A fully-loaded entity object.
 * @param array $field
 *   A field info array.
 * @param string $property_name
 *   The name of a Solr property to retrieve.
 *
 * @return
 *   An array containing values from Solr. Even the properties of single-value
 *   Solr fields are returned as an array.
 *
 * @see sarnia_field_formatter_view().
 */
function sarnia_field_get_property($entity, $field, $property_name) {
  $value = array();
  $field_name = $field['field_name'];
  if (isset($entity->{$field_name}) && isset($entity->{$field_name}[$property_name])) {

    // Return every property as if it is multi-valued, so that we don't have to
    // put that logic in every formatter.
    $value = $entity->{$field_name}[$property_name];
    $value = is_array($value) ? $value : array(
      $value,
    );
  }
  return $value;
}

/**
 * Get a list of Solr fields for a particular Search API index.
 *
 * @param string $index_machine_name
 *   The machine name of a Search API index.
 *
 * @param string $filter_method
 *   May be a string or NULL. If a string, the method of that name will be
 *   called on an instance of the SolrField class for each possible field.
 *   Especially useful are the values 'isPossibleKey', 'isSortable', and
 *   'isIndexed'.
 *
 * @return array
 *   An array of Solr fields, where the keys are Solr field names and the values
 *   are field labels. This array is suitable for use as a Form API #options
 *   property.
 */
function sarnia_index_get_options($index_machine_name, $filter_method = NULL) {
  $options = array();
  if ($index = search_api_index_load($index_machine_name)) {
    foreach ($index
      ->server()
      ->{$filter_method}() as $name => $field) {

      //@TODO Allow admins to provide labels for solr fields.
      $options[$field
        ->getName()] = $name;
    }
    asort($options);
  }
  return $options;
}

/**
 * Get and cache a list of retrievable fields for a particular Search API index.
 *
 * @see sarnia_index_get_options()
 */
function sarnia_index_get_field_options($index_machine_name, $reset = FALSE) {
  $options =& drupal_static(__FUNCTION__, array());
  if (!isset($options[$index_machine_name]) || $reset) {
    $options[$index_machine_name] = sarnia_index_get_options($index_machine_name, 'getDisplayFields');
  }
  return $options[$index_machine_name];
}

/**
 * Get and cache a list of filterable fields for a particular Search API index.
 *
 * @see sarnia_index_get_options()
 */
function sarnia_index_get_filter_options($index_machine_name, $reset = FALSE) {
  $options =& drupal_static(__FUNCTION__, array());
  if (!isset($options[$index_machine_name]) || $reset) {
    $options[$index_machine_name] = sarnia_index_get_options($index_machine_name, 'getFilterFields');
  }
  return $options[$index_machine_name];
}

/**
 * Implements HOOK_form_search_api_admin_index_edit_alter().
 * Implements HOOK_form_FORM_ID_alter().
 *
 * Disable the 'server' select box on Search API index configuration forms.
 */
function sarnia_form_search_api_admin_index_edit_alter(&$form, &$form_state) {
  if (($index = menu_get_object('search_api_index', 5)) && ($sarnia_type = sarnia_entity_type_load_by_index($index->machine_name))) {
    $form['server']['#disabled'] = TRUE;
    $form['server']['#description'] = t('This index uses the search server as a data source. You can not change the server on this type of index after it has been created.');
  }
}

/**
 * Implements hook_entity_load().
 *
 * Add Solr properties to Search API Index entities' fields if they're managed
 * by Sarnia.
 *
 * SearchApiIndex::getFields() returns only fields that are in both
 * $index->options['fields'] and the EntityAPI properties. This means that we
 * have to inject the Solr properties both in hook_entity_load() and in
 * hook_entity_property_info().
 * @see SearchApiIndex::getFields(), sarnia_entity_property_info()
 * @see http://drupal.org/node/1308638
 * @see http://drupalcode.org/project/search_api.git/commit/b5ed6b
 * @see http://drupalcode.org/project/search_api.git/commit/b53fec
 */
function sarnia_entity_load($entities, $type) {
  if ($type == 'search_api_index') {
    foreach ($entities as $entity) {

      // If this entity is associated with a sarnia type, override the index 'fields'.
      if ($entity
        ->server() && ($sarnia_type = sarnia_entity_type_load_by_index($entity->machine_name))) {
        $prev_fields = array();
        if (isset($entity->options['fields'])) {
          $prev_fields = $entity->options['fields'];
        }
        $entity->options['fields'] = array();
        foreach ($entity
          ->server()
          ->getRemoteFields() as $key => $field) {

          // Set a default boost factor.
          $boost = '1.0';
          if (!empty($prev_fields[$key]) && !empty($prev_fields[$key]['boost'])) {

            // Preserve the boost factor if the user set it in the UI.
            $boost = $prev_fields[$key]['boost'];
          }
          $entity->options['fields'][$key] = array(
            'name' => $key,
            'indexed' => $field
              ->isIndexed(),
            'type' => 'none',
            'boost' => $boost,
          );
        }
        foreach ($entity
          ->server()
          ->getFulltextFields() as $key => $field) {
          $entity->options['fields'][$key]['type'] = 'text';
        }

        // Set the type of date fields.
        foreach ($entity
          ->server()
          ->getRemoteFields() as $key => $field) {
          if ($field
            ->getType() == 'tdate') {
            $entity->options['fields'][$key]['type'] = 'date';
          }
        }
        ksort($entity->options['fields']);
      }
    }
  }
}

/**
 * Implements hook_views_api().
 */
function sarnia_views_api() {
  return array(
    'api' => '3.0-alpha1',
  );
}

/**
 * Implements hook_ctools_plugin_api().
 */
function sarnia_ctools_plugin_api($module, $api) {
  if ($module == 'openlayers' && ($api == 'openlayers_maps' || $api == 'openlayers_layers')) {
    return array(
      'version' => 1,
    );
  }
}

/**
 * Implements hook_search_api_server_delete().
 *
 * Delete any associated Sarnia entity type when a Search API server is deleted.
 */
function sarnia_search_api_server_delete(SearchApiServer $server) {
  if ($server->class == 'sarnia_solr_server') {
    $entity_type_machine_name = sarnia_entity_server_name_load($server->machine_name);

    // Delete the entity type.
    module_load_include('inc', 'sarnia', 'sarnia.entities');
    sarnia_entity_type_delete($entity_type_machine_name);

    // Rebuild the entity info cache and the menus, so that tabs provided by the
    // Field UI module disappear immediately.
    entity_info_cache_clear();
    menu_rebuild();

    // Tell the user what just happened.
    drupal_set_message(t('Deleted the Sarnia entity type and a Search API index for the %server_name server.', array(
      '%server_name' => $server->name,
    )));
  }
}

/**
 * Implements hook_search_api_service_info().
 *
 * Provide a Search API service so that we can customize Solr behavior.
 */
function sarnia_search_api_service_info() {
  $services['sarnia_solr_service'] = array(
    'name' => t('Sarnia Solr service'),
    'description' => t('Search-only connection to a Solr core.'),
    'class' => 'SarniaSolrService',
  );
  return $services;
}
function sarnia_element_add_combobox(&$element) {
  $element['#attributes']['class'][] = 'combobox';
}

/**
 * Build a Render API array from a Sarnia entity.
 *
 * @see node_view(), user_view()
 *
 * @TODO SarniaController should implement EntityAPIControllerInterface, and
 *       this should live in SarniaController::view()
 */
function sarnia_view(&$entity, $view_mode = 'default', $langcode = NULL) {
  if (!isset($langcode)) {
    $langcode = $GLOBALS['language_content']->language;
  }

  // Build field output in $entity->content.
  sarnia_build_content($entity, $view_mode, $langcode);

  // Initialize the render array.
  $build = $entity->content;

  // Remove the render array from the entity, so that it only appears once.
  unset($entity->content);

  // Add basic info to the render array.
  $build += array(
    '#theme' => 'sarnia_entity',
    '#entity' => $entity,
    '#view_mode' => $view_mode,
    '#language' => $langcode,
  );

  // Invoke hook_entity_view_alter().
  drupal_alter('entity_view', $build, $entity->type);
  return $build;
}
function sarnia_view_multiple($entities, $view_mode = 'default', $langcode = NULL) {
  $output = array();
  foreach ($entities as $entity) {
    $output[$entity->id] = sarnia_view($entity, $view_mode, $langcode);
  }
  return $output;
}

/**
 * Build a render array of field content on a Sarnia entity.
 *
 * @see node_build_content(), user_build_content()
 */
function sarnia_build_content($entity, $view_mode, $langcode = NULL) {
  if (!isset($langcode)) {
    $langcode = $GLOBALS['language_content']->language;
  }

  // Remove previously built content.
  $entity->content = array();

  // Add special Sarnia data.
  // @see sarnia_field_extra_fields()
  $entity->content['sarnia_id'] = array();
  $entity->content['sarnia_id']['title'] = array(
    '#type' => 'html_tag',
    '#tag' => 'h4',
    '#value' => t('Id'),
  );
  $entity->content['sarnia_id']['value'] = array(
    '#type' => 'html_tag',
    '#tag' => 'pre',
    '#value' => check_plain($entity->id),
  );
  $solr_document_field = '_data';
  foreach (current(field_info_instances($entity->type)) as $field_name => $instance) {
    $field_info = field_info_field($field_name);
    if ($field_info['type'] == 'sarnia') {
      $solr_document_field = $field_name;
      break;
    }
  }
  $entity->content['sarnia_solr_properties'] = array();
  $entity->content['sarnia_solr_properties']['title'] = array(
    '#type' => 'html_tag',
    '#tag' => 'h4',
    '#value' => t('Solr properties'),
  );
  $entity->content['sarnia_solr_properties']['value'] = array(
    '#type' => 'html_tag',
    '#tag' => 'pre',
    '#value' => check_plain(print_r($entity->{$solr_document_field}, TRUE)),
  );

  // Build field content.
  field_attach_prepare_view($entity->type, array(
    $entity->id => $entity,
  ), $view_mode, $langcode);
  entity_prepare_view($entity->type, array(
    $entity->id => $entity,
  ), $view_mode, $langcode);
  $entity->content += field_attach_view($entity->type, $entity, $view_mode, $langcode);

  // Invoke hooks.
  module_invoke_all('entity_view', $entity, $entity->type, $view_mode, $langcode);
}

/**
 * Preprocess function for displaying a Sarnia entity.
 *
 * @see sarnia-entity.tpl.php, template_preprocess_user_profile(), user-profile.tpl.php
 */
function template_preprocess_sarnia_entity(&$variables) {
  $entity = $variables['elements']['#entity'];

  // Helpful $content variable for templates.
  foreach (element_children($variables['elements']) as $key) {
    $variables['content'][$key] = $variables['elements'][$key];
  }

  // Preprocess fields.
  field_attach_preprocess($entity->type, $entity, $variables['elements'], $variables);
}

/**
 * Implements hook_sarnia_solr_service_schema().
 */
function sarnia_sarnia_solr_service_schema($conditions = array()) {
  $conditions += array(
    'enabled' => TRUE,
  );
  $query = db_select('sarnia_solr_service_schema', 'ss')
    ->fields('ss')
    ->orderBy('ss.behavior', 'ASC');
  foreach ($conditions as $field => $value) {
    $query
      ->condition('ss.' . $field, $value);
  }

  // Sort server-specific rules first.
  $query
    ->orderBy('ss.search_api_server', 'DESC');

  // Sort name matches first, then dynamicBase, then type.
  $rule_order = $query
    ->addExpression("ss.match_type = 'name'", 'sort_1');
  $query
    ->orderBy($rule_order, 'DESC');
  $rule_order = $query
    ->addExpression("ss.match_type = 'dynamicBase'", 'sort_2');
  $query
    ->orderBy($rule_order, 'DESC');
  $rule_order = $query
    ->addExpression("ss.match_type = 'type'", 'sort_3');
  $query
    ->orderBy($rule_order, 'DESC');
  return $query
    ->execute()
    ->fetchAll();
}
function sarnia_sarnia_solr_service_schema_rule_load($id) {
  return db_select('sarnia_solr_service_schema', 'ss')
    ->condition('id', $id)
    ->fields('ss')
    ->execute()
    ->fetch();
}

/**
 * Implements hook_form_FORM_ID_alter().
 *
 * Don't allow deleting indexes or servers until the associated Sarnia entity
 * type is deleted.
 */
function sarnia_form_search_api_admin_confirm_alter(&$form, &$form_state) {
  if ($form['type']['#value'] == 'index' && $form['action']['#value'] == 'delete') {
    $index = $form_state['build_info']['args'][2];
    if (($entity_type = sarnia_entity_type_load($index->machine_name)) && $entity_type['search_api_server'] == $index->server) {
      $form['info'] = array(
        '#type' => 'container',
        'info' => array(
          '#markup' => t("This index is created and managed by Sarnia. It will be deleted only when the Sarnia entity type is deleted. You can manage the Sarnia entity type by visiting the !link.", array(
            '!link' => l(t('the server configuration section'), "admin/config/search/search_api/server/{$index->server}/sarnia"),
          )),
        ),
      );
      $form['message']['#access'] = FALSE;
      $form['description']['#access'] = FALSE;
      $form['confirm']['#access'] = FALSE;
      $form['actions']['#access'] = FALSE;
    }
  }
  elseif ($form['type']['#value'] == 'server' && $form['action']['#value'] == 'delete') {
    $server = $form_state['build_info']['args'][2];
    if ($server->class == 'sarnia_solr_service') {
      $sarnia_type = sarnia_entity_server_name_load($server->machine_name);
      if ($sarnia_type) {

        // Don't allow deleting 'till the Sarnia type is deleted.
        $form['info'] = array(
          '#type' => 'container',
          'info' => array(
            '#markup' => t("This Search API Server has a Sarnia entity type enabled. Please delete the Sarnia entity type before deleting the server. You can manage the Sarnia entity type by visiting the !link.", array(
              '!link' => l(t('the Sarnia configuration tab'), "admin/config/search/search_api/server/{$server->machine_name}/sarnia"),
            )),
          ),
        );
        $form['message']['#access'] = FALSE;
        $form['description']['#access'] = FALSE;
        $form['confirm']['#access'] = FALSE;
        $form['actions']['#access'] = FALSE;
      }
    }
  }
}

/**
 * Implements hook_facetapi_query_types().
 *
 * Sarnia uses the 'standard' Solr query type so that it can use more advanced
 * query syntax; however, this requires passing quoted terms in facets, so we
 * need our own FacetAPI query type.
 */
function sarnia_facetapi_query_types() {
  return array(
    'sarnia_term' => array(
      'handler' => array(
        'class' => 'SarniaFacetapiTerm',
        'adapter' => 'search_api',
      ),
    ),
  );
}

/**
 * Implements hook_facetapi_info_alter().
 *
 * Add displayable Solr properties from Sarnia as facets.
 */
function sarnia_facetapi_facet_info_alter(&$facet_info, $searcher_info) {
  if ('search_api' == $searcher_info['adapter'] && ($sarnia_type = sarnia_entity_type_load_by_index($searcher_info['instance']))) {
    $index = search_api_index_load($searcher_info['instance']);
    $server = search_api_server_load($index->server);
    if ($server) {
      foreach ($server
        ->getDisplayFields() as $facet_name => $field) {

        // Do not overwrite facets that rely on Search API default facet
        // settings.
        if (in_array($field
          ->getType(), array(
          'tdate',
        ))) {
          continue;
        }
        $facet_info[$facet_name] = array(
          'name' => $facet_name,
          'label' => $facet_name,
          'description' => '',
          'field' => $facet_name,
          'field alias' => $facet_name,
          'field api name' => FALSE,
          'field api bundles' => array(),
          'query types' => array(
            'sarnia_term',
            'term',
          ),
          'alter callbacks' => array(),
          'dependency plugins' => array(
            'bundle',
            'role',
          ),
          'default widget' => FALSE,
          'allowed operators' => array(
            FACETAPI_OPERATOR_AND => TRUE,
            FACETAPI_OPERATOR_OR => TRUE,
          ),
          'facet missing allowed' => TRUE,
          'facet mincount allowed' => TRUE,
          'weight' => 0,
          'map callback' => FALSE,
          'map options' => array(),
          'hierarchy callback' => FALSE,
          'values callback' => FALSE,
          'min callback' => FALSE,
          'max callback' => FALSE,
          'default sorts' => array(
            array(
              'active',
              SORT_DESC,
            ),
            array(
              'count',
              SORT_DESC,
            ),
            array(
              'display',
              SORT_ASC,
            ),
          ),
        );
        if (module_exists('facetapi_bonus')) {
          $facet_info[$facet_name]['dependency plugins'] = array(
            'bundle',
            'role',
            'facet',
          );
        }
        foreach ($server
          ->getFulltextFields() as $key => $field) {
          unset($facet_info[$key]);
        }
      }
    }
  }
}

/**
 * Implements hook_search_api_facetapi_keys_alter().
 *
 * search_api_facetapi uses any query keys as the breadcrumb title. For Sarnia
 * queries, this ends up being a list of entity ids; the original page title
 * is more appropriate.
 */
function sarnia_search_api_facetapi_keys_alter(&$keys, $query) {

  // Only change keys for Sarnia indexes.
  if ($query instanceof SearchApiQuery && ($index = $query
    ->getIndex()) && sarnia_entity_type_load_by_index($index->machine_name)) {
    $keys = drupal_get_title();
  }
}

/**
 * Implements hook_features_pipe_alter().
 *
 * @param $pipe
 * @param $data
 * @param $export
 */
function sarnia_features_pipe_alter(&$pipe, $data, &$export) {

  // If exporting a Sarnia search index, also export the entity type and vice versa.
  if (in_array($export['component'], array(
    'search_api_index',
    'sarnia_entity_type',
  ))) {
    module_load_include('inc', 'sarnia', 'sarnia.entities');
    $types = _sarnia_entity_types();
    $depends = $export['component'] == 'sarnia_entity_type' ? 'search_api_index' : 'sarnia_entity_type';
    foreach ($data as $component) {
      if (in_array($component, array_keys($types))) {
        $export['features'][$depends][$component] = $component;
      }
    }
  }
}

/**
 * Implements hook_ctools_plugin_post_alter().
 */
function sarnia_ctools_plugin_post_alter(&$plugin, &$info) {
  if ($plugin['module'] == 'facetapi' && ($plugin['name'] == 'facetapi_checkbox_links' || $plugin['name'] == 'facetapi_links')) {
    $plugin['handler']['query types'][] = 'sarnia_term';
  }
  if ($plugin['module'] == 'facetapi_multiselect' && $plugin['name'] == 'facetapi_multiselect') {
    $plugin['handler']['query types'][] = 'sarnia_term';
  }
  if ($plugin['module'] == 'facetapi_select' && $plugin['name'] == 'facetapi_select_dropdowns') {
    $plugin['handler']['query types'][] = 'sarnia_term';
  }
}

Functions

Namesort descending Description
sarnia_build_content Build a render array of field content on a Sarnia entity.
sarnia_ctools_plugin_api Implements hook_ctools_plugin_api().
sarnia_ctools_plugin_post_alter Implements hook_ctools_plugin_post_alter().
sarnia_element_add_combobox
sarnia_entity_info Implements hook_entity_info().
sarnia_entity_load Implements hook_entity_load().
sarnia_entity_page Menu page callback to display a Sarnia entity.
sarnia_entity_property_info Implements hook_entity_property_info().
sarnia_entity_server_name_load Get a Sarnia entity type machine name given a Search API server machine name.
sarnia_entity_types Get information about all Sarnia-provided entities.
sarnia_entity_type_load Load bundle information about a specific Sarnia entity type.
sarnia_entity_type_load_by_index Load Sarnia entity type info given a Search API index machine name.
sarnia_facetapi_facet_info_alter Implements hook_facetapi_info_alter().
sarnia_facetapi_query_types Implements hook_facetapi_query_types().
sarnia_features_pipe_alter Implements hook_features_pipe_alter().
sarnia_field_extra_fields Implements hook_field_extra_fields().
sarnia_field_get_property Get the value of a Solr property from a 'sarnia' Solr document field.
sarnia_field_info Implements hook_field_info().
sarnia_field_load Implements hook_field_load().
sarnia_field_ui_menu_access_callback Check whether a Sarnia bundle Field UI sub-path is valid, and do the access check.
sarnia_field_ui_menu_load Transform the server name argument into entity type and bundle arguments for field_ui_menu_load().
sarnia_field_ui_menu_page_callback Load the appropriate form for a particular Sarnia bundle Field UI sub-path.
sarnia_field_widget_form Implements hook_field_widget_form().
sarnia_field_widget_info Implements hook_field_widget_info().
sarnia_form_search_api_admin_confirm_alter Implements hook_form_FORM_ID_alter().
sarnia_form_search_api_admin_index_edit_alter Implements HOOK_form_search_api_admin_index_edit_alter(). Implements HOOK_form_FORM_ID_alter().
sarnia_help Implements hook_help().
sarnia_index_get_field_options Get and cache a list of retrievable fields for a particular Search API index.
sarnia_index_get_filter_options Get and cache a list of filterable fields for a particular Search API index.
sarnia_index_get_options Get a list of Solr fields for a particular Search API index.
sarnia_load Menu load callback; load a Sarnia entity.
sarnia_menu Implements hook_menu().
sarnia_menu_alter Implements hook_menu_alter().
sarnia_permission Implements hook_permission().
sarnia_sarnia_solr_service_schema Implements hook_sarnia_solr_service_schema().
sarnia_sarnia_solr_service_schema_rule_load
sarnia_search_api_facetapi_keys_alter Implements hook_search_api_facetapi_keys_alter().
sarnia_search_api_server_delete Implements hook_search_api_server_delete().
sarnia_search_api_service_info Implements hook_search_api_service_info().
sarnia_theme Implements hook_theme().
sarnia_uri Provide a path for a particular entity.
sarnia_view Build a Render API array from a Sarnia entity.
sarnia_views_api Implements hook_views_api().
sarnia_view_multiple
template_preprocess_sarnia_entity Preprocess function for displaying a Sarnia entity.
_sarnia_entity_manage_form_access Determine whether to show the 'Sarnia' tab on a particular Search API server page.

Classes

Namesort descending Description
SarniaController Controller class for Sarnia entities.