diff --git a/LICENSE.txt b/LICENSE.txt
new file mode 100644
index 0000000..d159169
--- /dev/null
+++ b/LICENSE.txt
@@ -0,0 +1,339 @@
+                    GNU GENERAL PUBLIC LICENSE
+                       Version 2, June 1991
+
+ Copyright (C) 1989, 1991 Free Software Foundation, Inc.,
+ 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
+ Everyone is permitted to copy and distribute verbatim copies
+ of this license document, but changing it is not allowed.
+
+                            Preamble
+
+  The licenses for most software are designed to take away your
+freedom to share and change it.  By contrast, the GNU General Public
+License is intended to guarantee your freedom to share and change free
+software--to make sure the software is free for all its users.  This
+General Public License applies to most of the Free Software
+Foundation's software and to any other program whose authors commit to
+using it.  (Some other Free Software Foundation software is covered by
+the GNU Lesser General Public License instead.)  You can apply it to
+your programs, too.
+
+  When we speak of free software, we are referring to freedom, not
+price.  Our General Public Licenses are designed to make sure that you
+have the freedom to distribute copies of free software (and charge for
+this service if you wish), that you receive source code or can get it
+if you want it, that you can change the software or use pieces of it
+in new free programs; and that you know you can do these things.
+
+  To protect your rights, we need to make restrictions that forbid
+anyone to deny you these rights or to ask you to surrender the rights.
+These restrictions translate to certain responsibilities for you if you
+distribute copies of the software, or if you modify it.
+
+  For example, if you distribute copies of such a program, whether
+gratis or for a fee, you must give the recipients all the rights that
+you have.  You must make sure that they, too, receive or can get the
+source code.  And you must show them these terms so they know their
+rights.
+
+  We protect your rights with two steps: (1) copyright the software, and
+(2) offer you this license which gives you legal permission to copy,
+distribute and/or modify the software.
+
+  Also, for each author's protection and ours, we want to make certain
+that everyone understands that there is no warranty for this free
+software.  If the software is modified by someone else and passed on, we
+want its recipients to know that what they have is not the original, so
+that any problems introduced by others will not reflect on the original
+authors' reputations.
+
+  Finally, any free program is threatened constantly by software
+patents.  We wish to avoid the danger that redistributors of a free
+program will individually obtain patent licenses, in effect making the
+program proprietary.  To prevent this, we have made it clear that any
+patent must be licensed for everyone's free use or not licensed at all.
+
+  The precise terms and conditions for copying, distribution and
+modification follow.
+
+                    GNU GENERAL PUBLIC LICENSE
+   TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION
+
+  0. This License applies to any program or other work which contains
+a notice placed by the copyright holder saying it may be distributed
+under the terms of this General Public License.  The "Program", below,
+refers to any such program or work, and a "work based on the Program"
+means either the Program or any derivative work under copyright law:
+that is to say, a work containing the Program or a portion of it,
+either verbatim or with modifications and/or translated into another
+language.  (Hereinafter, translation is included without limitation in
+the term "modification".)  Each licensee is addressed as "you".
+
+Activities other than copying, distribution and modification are not
+covered by this License; they are outside its scope.  The act of
+running the Program is not restricted, and the output from the Program
+is covered only if its contents constitute a work based on the
+Program (independent of having been made by running the Program).
+Whether that is true depends on what the Program does.
+
+  1. You may copy and distribute verbatim copies of the Program's
+source code as you receive it, in any medium, provided that you
+conspicuously and appropriately publish on each copy an appropriate
+copyright notice and disclaimer of warranty; keep intact all the
+notices that refer to this License and to the absence of any warranty;
+and give any other recipients of the Program a copy of this License
+along with the Program.
+
+You may charge a fee for the physical act of transferring a copy, and
+you may at your option offer warranty protection in exchange for a fee.
+
+  2. You may modify your copy or copies of the Program or any portion
+of it, thus forming a work based on the Program, and copy and
+distribute such modifications or work under the terms of Section 1
+above, provided that you also meet all of these conditions:
+
+    a) You must cause the modified files to carry prominent notices
+    stating that you changed the files and the date of any change.
+
+    b) You must cause any work that you distribute or publish, that in
+    whole or in part contains or is derived from the Program or any
+    part thereof, to be licensed as a whole at no charge to all third
+    parties under the terms of this License.
+
+    c) If the modified program normally reads commands interactively
+    when run, you must cause it, when started running for such
+    interactive use in the most ordinary way, to print or display an
+    announcement including an appropriate copyright notice and a
+    notice that there is no warranty (or else, saying that you provide
+    a warranty) and that users may redistribute the program under
+    these conditions, and telling the user how to view a copy of this
+    License.  (Exception: if the Program itself is interactive but
+    does not normally print such an announcement, your work based on
+    the Program is not required to print an announcement.)
+
+These requirements apply to the modified work as a whole.  If
+identifiable sections of that work are not derived from the Program,
+and can be reasonably considered independent and separate works in
+themselves, then this License, and its terms, do not apply to those
+sections when you distribute them as separate works.  But when you
+distribute the same sections as part of a whole which is a work based
+on the Program, the distribution of the whole must be on the terms of
+this License, whose permissions for other licensees extend to the
+entire whole, and thus to each and every part regardless of who wrote it.
+
+Thus, it is not the intent of this section to claim rights or contest
+your rights to work written entirely by you; rather, the intent is to
+exercise the right to control the distribution of derivative or
+collective works based on the Program.
+
+In addition, mere aggregation of another work not based on the Program
+with the Program (or with a work based on the Program) on a volume of
+a storage or distribution medium does not bring the other work under
+the scope of this License.
+
+  3. You may copy and distribute the Program (or a work based on it,
+under Section 2) in object code or executable form under the terms of
+Sections 1 and 2 above provided that you also do one of the following:
+
+    a) Accompany it with the complete corresponding machine-readable
+    source code, which must be distributed under the terms of Sections
+    1 and 2 above on a medium customarily used for software interchange; or,
+
+    b) Accompany it with a written offer, valid for at least three
+    years, to give any third party, for a charge no more than your
+    cost of physically performing source distribution, a complete
+    machine-readable copy of the corresponding source code, to be
+    distributed under the terms of Sections 1 and 2 above on a medium
+    customarily used for software interchange; or,
+
+    c) Accompany it with the information you received as to the offer
+    to distribute corresponding source code.  (This alternative is
+    allowed only for noncommercial distribution and only if you
+    received the program in object code or executable form with such
+    an offer, in accord with Subsection b above.)
+
+The source code for a work means the preferred form of the work for
+making modifications to it.  For an executable work, complete source
+code means all the source code for all modules it contains, plus any
+associated interface definition files, plus the scripts used to
+control compilation and installation of the executable.  However, as a
+special exception, the source code distributed need not include
+anything that is normally distributed (in either source or binary
+form) with the major components (compiler, kernel, and so on) of the
+operating system on which the executable runs, unless that component
+itself accompanies the executable.
+
+If distribution of executable or object code is made by offering
+access to copy from a designated place, then offering equivalent
+access to copy the source code from the same place counts as
+distribution of the source code, even though third parties are not
+compelled to copy the source along with the object code.
+
+  4. You may not copy, modify, sublicense, or distribute the Program
+except as expressly provided under this License.  Any attempt
+otherwise to copy, modify, sublicense or distribute the Program is
+void, and will automatically terminate your rights under this License.
+However, parties who have received copies, or rights, from you under
+this License will not have their licenses terminated so long as such
+parties remain in full compliance.
+
+  5. You are not required to accept this License, since you have not
+signed it.  However, nothing else grants you permission to modify or
+distribute the Program or its derivative works.  These actions are
+prohibited by law if you do not accept this License.  Therefore, by
+modifying or distributing the Program (or any work based on the
+Program), you indicate your acceptance of this License to do so, and
+all its terms and conditions for copying, distributing or modifying
+the Program or works based on it.
+
+  6. Each time you redistribute the Program (or any work based on the
+Program), the recipient automatically receives a license from the
+original licensor to copy, distribute or modify the Program subject to
+these terms and conditions.  You may not impose any further
+restrictions on the recipients' exercise of the rights granted herein.
+You are not responsible for enforcing compliance by third parties to
+this License.
+
+  7. If, as a consequence of a court judgment or allegation of patent
+infringement or for any other reason (not limited to patent issues),
+conditions are imposed on you (whether by court order, agreement or
+otherwise) that contradict the conditions of this License, they do not
+excuse you from the conditions of this License.  If you cannot
+distribute so as to satisfy simultaneously your obligations under this
+License and any other pertinent obligations, then as a consequence you
+may not distribute the Program at all.  For example, if a patent
+license would not permit royalty-free redistribution of the Program by
+all those who receive copies directly or indirectly through you, then
+the only way you could satisfy both it and this License would be to
+refrain entirely from distribution of the Program.
+
+If any portion of this section is held invalid or unenforceable under
+any particular circumstance, the balance of the section is intended to
+apply and the section as a whole is intended to apply in other
+circumstances.
+
+It is not the purpose of this section to induce you to infringe any
+patents or other property right claims or to contest validity of any
+such claims; this section has the sole purpose of protecting the
+integrity of the free software distribution system, which is
+implemented by public license practices.  Many people have made
+generous contributions to the wide range of software distributed
+through that system in reliance on consistent application of that
+system; it is up to the author/donor to decide if he or she is willing
+to distribute software through any other system and a licensee cannot
+impose that choice.
+
+This section is intended to make thoroughly clear what is believed to
+be a consequence of the rest of this License.
+
+  8. If the distribution and/or use of the Program is restricted in
+certain countries either by patents or by copyrighted interfaces, the
+original copyright holder who places the Program under this License
+may add an explicit geographical distribution limitation excluding
+those countries, so that distribution is permitted only in or among
+countries not thus excluded.  In such case, this License incorporates
+the limitation as if written in the body of this License.
+
+  9. The Free Software Foundation may publish revised and/or new versions
+of the General Public License from time to time.  Such new versions will
+be similar in spirit to the present version, but may differ in detail to
+address new problems or concerns.
+
+Each version is given a distinguishing version number.  If the Program
+specifies a version number of this License which applies to it and "any
+later version", you have the option of following the terms and conditions
+either of that version or of any later version published by the Free
+Software Foundation.  If the Program does not specify a version number of
+this License, you may choose any version ever published by the Free Software
+Foundation.
+
+  10. If you wish to incorporate parts of the Program into other free
+programs whose distribution conditions are different, write to the author
+to ask for permission.  For software which is copyrighted by the Free
+Software Foundation, write to the Free Software Foundation; we sometimes
+make exceptions for this.  Our decision will be guided by the two goals
+of preserving the free status of all derivatives of our free software and
+of promoting the sharing and reuse of software generally.
+
+                            NO WARRANTY
+
+  11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY
+FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW.  EXCEPT WHEN
+OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES
+PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED
+OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
+MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE.  THE ENTIRE RISK AS
+TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU.  SHOULD THE
+PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING,
+REPAIR OR CORRECTION.
+
+  12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
+WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR
+REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES,
+INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING
+OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED
+TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY
+YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER
+PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE
+POSSIBILITY OF SUCH DAMAGES.
+
+                     END OF TERMS AND CONDITIONS
+
+            How to Apply These Terms to Your New Programs
+
+  If you develop a new program, and you want it to be of the greatest
+possible use to the public, the best way to achieve this is to make it
+free software which everyone can redistribute and change under these terms.
+
+  To do so, attach the following notices to the program.  It is safest
+to attach them to the start of each source file to most effectively
+convey the exclusion of warranty; and each file should have at least
+the "copyright" line and a pointer to where the full notice is found.
+
+    <one line to give the program's name and a brief idea of what it does.>
+    Copyright (C) <year>  <name of author>
+
+    This program is free software; you can redistribute it and/or modify
+    it under the terms of the GNU General Public License as published by
+    the Free Software Foundation; either version 2 of the License, or
+    (at your option) any later version.
+
+    This program is distributed in the hope that it will be useful,
+    but WITHOUT ANY WARRANTY; without even the implied warranty of
+    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+    GNU General Public License for more details.
+
+    You should have received a copy of the GNU General Public License along
+    with this program; if not, write to the Free Software Foundation, Inc.,
+    51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
+
+Also add information on how to contact you by electronic and paper mail.
+
+If the program is interactive, make it output a short notice like this
+when it starts in an interactive mode:
+
+    Gnomovision version 69, Copyright (C) year name of author
+    Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
+    This is free software, and you are welcome to redistribute it
+    under certain conditions; type `show c' for details.
+
+The hypothetical commands `show w' and `show c' should show the appropriate
+parts of the General Public License.  Of course, the commands you use may
+be called something other than `show w' and `show c'; they could even be
+mouse-clicks or menu items--whatever suits your program.
+
+You should also get your employer (if you work as a programmer) or your
+school, if any, to sign a "copyright disclaimer" for the program, if
+necessary.  Here is a sample; alter the names:
+
+  Yoyodyne, Inc., hereby disclaims all copyright interest in the program
+  `Gnomovision' (which makes passes at compilers) written by James Hacker.
+
+  <signature of Ty Coon>, 1 April 1989
+  Ty Coon, President of Vice
+
+This General Public License does not permit incorporating your program into
+proprietary programs.  If your program is a subroutine library, you may
+consider it more useful to permit linking proprietary applications with the
+library.  If this is what you want to do, use the GNU Lesser General
+Public License instead of this License.
diff --git a/rdf_indexer-add_virtuoso_support-2029717-8.patch b/rdf_indexer-add_virtuoso_support-2029717-8.patch
new file mode 100644
index 0000000..6ef7543
--- /dev/null
+++ b/rdf_indexer-add_virtuoso_support-2029717-8.patch
@@ -0,0 +1,785 @@
+diff --git a/rdf_indexer.info b/rdf_indexer.info
+index a904e7f..38739dc 100644
+--- a/rdf_indexer.info
++++ b/rdf_indexer.info
+@@ -6,5 +6,8 @@ dependencies[] = search_api
+ core = 7.x
+ package = RDF
+ 
++configure = admin/config/search/search_api
++
+ files[] = service.inc
+ files[] = includes/callback_entity_public.inc
++files[] = service.virtuoso.inc
+diff --git a/rdf_indexer.module b/rdf_indexer.module
+index b3ed750..f492b85 100644
+--- a/rdf_indexer.module
++++ b/rdf_indexer.module
+@@ -13,6 +13,11 @@ function rdf_indexer_search_api_service_info() {
+     'description' => t('<p>Index items using an RDF store server.</p>'),
+     'class' => 'RdfIndexerArc2StoreService',
+   );
++  $services['rdf_virtuoso_service'] = array(
++    'name' => t('RDF indexer service for Virtuoso'),
++    'description' => t('<p>Index items using the Virtuoso triplestore.</p>'),
++    'class' => 'RdfIndexerVirtuosoService',
++  );
+   return $services;
+ }
+ 
+diff --git a/service.inc b/service.inc
+index f0d5b21..acc4d40 100644
+--- a/service.inc
++++ b/service.inc
+@@ -3,129 +3,32 @@
+ /**
+  * Search service class using an RDF store server.
+  */
+-class RdfIndexerArc2StoreService extends SearchApiAbstractService {
++class RdfIndexerBaseService extends SearchApiAbstractService {
+ 
+   public function __construct(SearchApiServer $server) {
+     parent::__construct($server);
+   }
+-
+-  /**
+-   * Form callback. Might be called on an uninitialized object - in this case,
+-   * the form is for configuring a newly created server.
+-   *
+-   * Returns an empty form by default.
+-   *
+-   * @return array
+-   *   A form array for setting service-specific options.
++  
++  /*
++   * Return a representation of the triplestore
+    */
+-  public function configurationForm(array $form, array &$form_state) {
+-    ctools_include('export');
+-    if (module_exists('arc2_store')) {
+-      foreach (ctools_export_crud_load_all('arc2_store_settings') as $store) {
+-        if (empty($store->disabled)) {
+-          $options[$store->store_id] = $store->label . ' (' . $store->store_id . ')';
+-        }
+-      }
+-    }
+-
+-    if (empty($options)) {
+-      $form['store_id'] = array(
+-        '#markup' => t('No ARC2 store found. Please install the ARC2 store module and create some stores.'),
+-      );
+-    }
+-    else {
+-      $form['store_id'] = array(
+-        '#type' => 'select',
+-        '#title' => t('ARC2 store'),
+-        '#description' => t('The ARC2 store where the data should be indexed.'),
+-        '#options' => $options,
+-        '#default_value' => $this->options['store_id'],
+-        '#required' => TRUE,
+-      );
+-    }
+-
+-    return $form;
+-  }
+-
+-  /**
+-   * View this server's settings. Output can be HTML or a render array, a <dl>
+-   * listing all relevant settings is preferred.
+-   *
+-   * The default implementation does a crude output as a definition list, with
+-   * option names taken from the configuration form.
+-   */
+-  public function viewSettings() {
+-    $output = '';
+-    $store = arc2_store_settings_load($this->options['store_id']);
+-    if (!empty($store->settings['endpoint_enabled'])) {
+-      $url = url($store->settings['endpoint_path'], array('absolute' => TRUE));
+-      $output .= "<dl>\n  <dt>";
+-      $output .= t('SPARQL endpoint');
+-      $output .= "</dt>\n  <dd>";
+-      $output .= l($url, $url);
+-      $output .= '</dd>';
+-      $output .= "\n</dl>";
+-    }
+-
+-    return $output;
++  public function getStore(SearchApiIndex $index) {
++    return FALSE;
+   }
+-
+-  /**
+-   * Notifies this server that it is about to be deleted from the database and
+-   * should therefore clean up, if appropriate.
+-   *
+-   * Note that you shouldn't call the server's save() method, or any
+-   * methods that might do that, from inside of this method as the server isn't
+-   * present in the database anymore at this point.
+-   *
+-   * By default, deletes all indexes from this server.
++  /*
++   * Index an item to the given store
+    */
+-  public function preDelete() {
+-    // Only react on real deletes, not on reverts.
+-    // @see https://drupal.org/node/1414078
+-    // This method could be removed once the above issue is fixed.
+-    if ($this->server->hasStatus(ENTITY_IN_CODE)) {
+-      return;
+-    }
+-    $indexes = search_api_index_load_multiple(FALSE, array('server' => $this->server->machine_name));
+-    foreach ($indexes as $index) {
+-      $this->removeIndex($index);
+-    }
+-  }
+-
+-  /**
+-   * Add a new index to this server.
+-   *
+-   * @param SearchApiIndex $index
+-   *   The index to add.
++  public function indexItem($item, $store, $graph = FALSE) {}
++  /*
++   * Delete an item from the given store
+    */
+-  public function addIndex(SearchApiIndex $index) {
+-    if (module_exists('search_api_multi') && module_exists('search_api_views')) {
+-      views_invalidate_cache();
+-    }
+-  }
+-
+-  /**
+-   * Notify the server that the indexed field settings for the index have
+-   * changed.
+-   * If any user action is necessary as a result of this, the method should
+-   * use drupal_set_message() to notify the user.
+-   *
+-   * @param SearchApiIndex $index
+-   *   The updated index.
+-   *
+-   * @return
+-   *   TRUE, if this change affected the server in any way that forces it to
+-   *   re-index the content. FALSE otherwise.
++  public function deleteItem($item, $store, $graph = FALSE) {}
++  /*
++   * Clear a graph from the given store
+    */
+-  public function fieldsUpdated(SearchApiIndex $index) {
+-    if (module_exists('search_api_multi') && module_exists('search_api_views')) {
+-      views_invalidate_cache();
+-    }
+-    return TRUE;
+-  }
+-
+-  /**
++  public function clearGraph($store, $graph = FALSE) {}
++  
++    /**
+    * Index the specified items.
+    *
+    * @param SearchApiIndex $index
+@@ -155,8 +58,8 @@ class RdfIndexerArc2StoreService extends SearchApiAbstractService {
+    *   If indexing was prevented by a fundamental configuration error.
+    */
+   public function indexItems(SearchApiIndex $index, array $items) {
+-    // Loads ARC2 store this index is connected to.
+-    $store = arc2_store_get_store($index->server()->options['store_id']);
++    
++    $store = $this->getStore($index);
+ 
+     // @todo use documents and merge them before sending to ARC2.
+     // $documents = array();
+@@ -166,7 +69,7 @@ class RdfIndexerArc2StoreService extends SearchApiAbstractService {
+       try {
+         // Builds an RDF resource for the entity.
+         $rdf = rdfx_get_rdf_model($index->item_type, $id);
+-        $store->insert($rdf->index, $rdf->uri);
++        $this->indexItem($rdf, $store);
+         $ret[] = $id;
+       }
+       catch (Exception $e) {
+@@ -175,7 +78,7 @@ class RdfIndexerArc2StoreService extends SearchApiAbstractService {
+     }
+     return $ret;
+   }
+-
++  
+   /**
+    * Delete items from an index on this server.
+    *
+@@ -192,30 +95,199 @@ class RdfIndexerArc2StoreService extends SearchApiAbstractService {
+    *   this server should be cleared (then, $ids has to be 'all').
+    */
+   public function deleteItems($ids = 'all', SearchApiIndex $index = NULL) {
+-    // Loads ARC2 store this index is connected to.
+-    $store = arc2_store_get_store($index->server()->options['store_id']);
+-
++  
++    $store = $this->getStore($index);
++    
+     try {
+       // Emtpy the local store.
+       if ($ids === 'all') {
+-        $store->reset();
++        $this->clearGraph($store);
+       }
+       elseif (is_array($ids)) {
+         // Contructs the URI of the graph for each entity ID and deletes it.
+-        foreach($ids as $id) {
++        foreach ($ids as $id) {
+           $entity = entity_load_single($index->item_type, $id);
+           $uri = rdfx_resource_uri($index->item_type, $entity);
+-          $store->delete('', $uri);
++          $this->deleteItem($uri, $store);
+         }
+       }
+     }
+-    catch(Exception $e) {
++    catch (Exception $e) {
+       watchdog_exception('rdf_indexer', $e, '%type while deleting items from server @server: !message in %function (line %line of %file).', array('@server' => $this->server->name));
+     }
+   }
+ 
++  
++  /**
++   * Notifies this server that it is about to be deleted from the database and
++   * should therefore clean up, if appropriate.
++   *
++   * Note that you shouldn't call the server's save() method, or any
++   * methods that might do that, from inside of this method as the server isn't
++   * present in the database anymore at this point.
++   *
++   * By default, deletes all indexes from this server.
++   */
++  public function preDelete() {
++    // Only react on real deletes, not on reverts.
++    // @see https://drupal.org/node/1414078
++    // This method could be removed once the above issue is fixed.
++    if ($this->server->hasStatus(ENTITY_IN_CODE)) {
++      return;
++    }
++    $indexes = search_api_index_load_multiple(FALSE, array('server' => $this->server->machine_name));
++    foreach ($indexes as $index) {
++      $this->removeIndex($index);
++    }
++  }
++  
++  /**
++   * Add a new index to this server.
++   *
++   * @param SearchApiIndex $index
++   *   The index to add.
++   */
++  public function addIndex(SearchApiIndex $index) {
++    if (module_exists('search_api_multi') && module_exists('search_api_views')) {
++      views_invalidate_cache();
++    }
++  }
++  
++  /**
++   * Notify the server that the indexed field settings for the index have
++   * changed.
++   * If any user action is necessary as a result of this, the method should
++   * use drupal_set_message() to notify the user.
++   *
++   * @param SearchApiIndex $index
++   *   The updated index.
++   *
++   * @return
++   *   TRUE, if this change affected the server in any way that forces it to
++   *   re-index the content. FALSE otherwise.
++   */
++  public function fieldsUpdated(SearchApiIndex $index) {
++    if (module_exists('search_api_multi') && module_exists('search_api_views')) {
++      views_invalidate_cache();
++    }
++    return TRUE;
++  }
++  
+   public function search(SearchApiQueryInterface $query) {
+     throw new SearchApiException(t('The RDF indexer service does not support search. Please query the SPARQL endpoint directly if the RDF store provide such service.'));
+   }
++}
++
++/**
++ * Search service class using an RDF store server.
++ */
++class RdfIndexerArc2StoreService extends RdfIndexerBaseService {
++
++  /*
++   * Loads ARC2 store this index is connected to.
++   */
++  public function getStore(SearchApiIndex $index) {
++    return arc2_store_get_store($index->server()->options['store_id']);
++  }
+ 
++  /**
++   * Form callback. Might be called on an uninitialized object - in this case,
++   * the form is for configuring a newly created server.
++   *
++   * Returns an empty form by default.
++   *
++   * @return array
++   *   A form array for setting service-specific options.
++   */
++  public function configurationForm(array $form, array &$form_state) {
++    ctools_include('export');
++    if (module_exists('arc2_store')) {
++      foreach (ctools_export_crud_load_all('arc2_store_settings') as $store) {
++        if (empty($store->disabled)) {
++          $options[$store->store_id] = $store->label . ' (' . $store->store_id . ')';
++        }
++      }
++    }
++
++    if (empty($options)) {
++      $form['store_id'] = array(
++        '#markup' => t('No ARC2 store found. Please install the ARC2 store module and create some stores.'),
++      );
++    }
++    else {
++      $form['store_id'] = array(
++        '#type' => 'select',
++        '#title' => t('ARC2 store'),
++        '#description' => t('The ARC2 store where the data should be indexed.'),
++        '#options' => $options,
++        '#default_value' => $this->options['store_id'],
++        '#required' => TRUE,
++      );
++    }
++    return $form;
++  }
++
++  /**
++   * View this server's settings. Output can be HTML or a render array, a <dl>
++   * listing all relevant settings is preferred.
++   *
++   * The default implementation does a crude output as a definition list, with
++   * option names taken from the configuration form.
++   */
++  public function viewSettings() {
++    $output = '';
++    $store = arc2_store_settings_load($this->options['store_id']);
++    if (!empty($store->settings['endpoint_enabled'])) {
++      $url = url($store->settings['endpoint_path'], array('absolute' => TRUE));
++      $output .= "<dl>\n  <dt>";
++      $output .= t('SPARQL endpoint');
++      $output .= "</dt>\n  <dd>";
++      $output .= l($url, $url);
++      $output .= '</dd>';
++      $output .= "\n</dl>";
++    }
++
++    return $output;
++  }
++  
++  /*
++   * Index an item to the given store
++   *
++   * @param $item
++   *   The rdfx_get_rdf_model() resource
++   * @param $store
++   *   The ARC2 Store
++   * @param $graph
++   *   The optional graph URI to update (unused for ARC2)
++   */
++  public function indexItem($item, $store, $graph = FALSE) {
++    $store->insert($item->index, $item->uri);
++  }
++  
++  /*
++   * Delete an item from the given store
++   *
++   * @param $item
++   *   The rdfx_get_rdf_model() resource
++   * @param $store
++   *   The ARC2 Store
++   * @param $graph
++   *   The optional graph URI to update (unused for ARC2)
++   */
++  public function deleteItem($item, $store, $graph = FALSE) {
++    $store->delete('', $item);
++  }
++  
++  /*
++   * Clear the ARC2 store
++   *
++   * @param $store
++   *   The ARC2 Store
++   * @param $graph
++   *   The optional graph URI to update (unused for ARC2)
++   */
++  public function clearGraph($store, $graph = FALSE) {
++    $store->reset();
++  }
++  
+ }
+diff --git a/service.virtuoso.inc b/service.virtuoso.inc
+new file mode 100644
+index 0000000..27481a2
+--- /dev/null
++++ b/service.virtuoso.inc
+@@ -0,0 +1,362 @@
++<?php
++
++/**
++ * Search service class using an RDF store server.
++ */
++class RdfIndexerVirtuosoService extends RdfIndexerBaseService {
++  
++  /*
++   * The external store
++   */
++  public function getStore(SearchApiIndex $index) {
++    return $this->options;
++  }
++  
++  public function getTriplestoreUrl() {
++    if ( !isset($this->options['url']) ) {
++      watchdog('rdf_indexer', "The URL of the Virtuoso Server has not been specified.", WATCHDOG_WARNING);
++    }
++    
++    return $this->options['url'];
++  }
++  
++  public function getRequestHeaders() {
++    $headers = array();
++    if ( !empty($this->options['authorization']) ) {
++      $headers['Authorization'] = $this->options['authorization'];
++    }
++    return $headers;
++  }
++
++  /**
++   * Form callback. Might be called on an uninitialized object - in this case,
++   * the form is for configuring a newly created server.
++   *
++   * Returns an empty form by default.
++   *
++   * @return array
++   *   A form array for setting service-specific options.
++   */
++  public function configurationForm(array $form, array &$form_state) {
++    global $base_url;
++    
++    $settings = isset($this->options) ? $this->options : array();
++    $form['url'] = array(
++      '#type' => 'textfield',
++      '#title' => t('Virtuoso SPARQL Endpoint URL'),
++      '#description' => t('The Virtuoso SPARQL Endpoint URL where data will be inserted and deleted via basic authentication. Typically, http://example.com:8890/sparql-auth'),
++      '#default_value' => isset($settings['url']) ? $settings['url'] : '',
++      '#required' => TRUE,
++    );
++    $form['graph'] = array(
++      '#type' => 'textfield',
++      '#title' => t('Graph URI'),
++      '#description' => t('The URI of the graph where data will be inserted and deleted.'),
++      '#default_value' => isset($settings['graph']) ? $settings['graph'] : $base_url,
++      '#required' => TRUE,
++    );
++    $form['test_query'] = array(
++      '#type' => 'textfield',
++      '#title' => t('Test SPARQL Query'),
++      '#description' => t('A test query that can be used to verify a connection can be established.'),
++      '#default_value' => isset($settings['test_query']) ? $settings['test_query'] : 'SELECT ?s WHERE {?s ?p ?o} LIMIT 1',
++      '#required' => TRUE,
++    );
++    $form['query_params'] = array(
++      '#type' => 'textfield',
++      '#title' => t('Additional Query Parameters'),
++      '#description' => t('Specify other query parameters that should be added to SPARQL Update requests.'),
++      '#default_value' => isset($settings['query_params']) ? $settings['query_params'] : 'default-graph-uri=&format=text%2Fhtml&timeout=0&debug=on',
++    );
++    $form['username'] = array(
++      '#type' => 'textfield',
++      '#title' => t('Username'),
++      '#description' => t('The username of the Virtuoso account with SPARQL/Update privileges'),
++      '#default_value' => isset($settings['username']) ? $settings['username'] : '',
++      '#required' => TRUE,
++    );   
++    //assume that 'Administer Search API' permission is valid enough to view the password 
++    $form['credential'] = array(
++      '#type' => 'textfield',
++      '#title' => t('Credential'),
++      '#description' => t('The credential of the Virtuoso account with SPARQL/Update privileges'),
++      '#default_value' => isset($settings['credential']) ? $settings['credential'] : '',
++      '#required' => TRUE,
++    ); 
++    $form['authorization'] = array(
++      '#type' => 'hidden',
++      '#value' => isset($settings['authorization']) ? $settings['authorization'] : '',
++    );    
++    return $form;
++  }
++
++  /**
++   * View this server's settings. Output can be HTML or a render array, a <dl>
++   * listing all relevant settings is preferred.
++   *
++   * The default implementation does a crude output as a definition list, with
++   * option names taken from the configuration form.
++   */
++  public function viewSettings() {
++    $settings = isset($this->options) ? $this->options : array();
++    if ( !empty($settings) ) {
++      require './includes/password.inc';
++      $output = "<dl>\n  <dt>";
++      $output .= t('Virtuoso SPARQL endpoint');
++      $output .= "</dt>\n  <dd>";
++      $output .= l($settings['url'], $settings['url']);
++      $output .= "</dd>\n  <dt>";
++      $output .= t('Graph URI');
++      $output .= "</dt>\n<dd>";
++      $output .= $settings['graph'];
++      $output .= "</dd>\n  <dt>";
++      $output .= t('Test Query');
++      $output .= "</dt>\n<dd>";
++      $output .= $settings['test_query'];
++      $output .= "</dd>\n  <dt>";
++      $output .= t('Additional Query Parameters');
++      $output .= "</dt>\n<dd>";
++      $output .= $settings['query_params'];
++      $output .= "</dd>\n  <dt>";
++      $output .= t('Username');
++      $output .= "</dt>\n  <dd>";
++      $output .= $settings['username'];
++      $output .= "</dd>\n  <dt>";
++      $output .= t('Credential');
++      $output .= "</dt>\n <dd>";
++      $output .= user_hash_password($settings['credential']);
++      $output .= "</dd>\n</dl>";
++      return $output;
++    } 
++    else {
++      return 'Virtuoso has not been configured.';
++    }
++  }
++
++  /*
++   * Index an item to the given store
++   *
++   * @param $item
++   *   The rdfx_get_rdf_model() resource
++   * @param $store
++   *   The Virtuoso Store options from the SearchApiServer
++   * @param $graph
++   *   The optional graph URI to update
++   * @param $options
++   *    Array provided to expansion of the function
++   *    Currently, it supports returning the query string
++   */
++  public function indexItem($item, $store, $graph = FALSE, $options = array()) {
++  
++    $graph = $graph ? $graph : $store['graph'];
++    $query = 'INSERT DATA INTO GRAPH <' . $graph . '> { ';
++    foreach ($item->index as $subject => $triples) {
++      foreach ($triples as $predicate => $value) {
++        foreach ($value as $triple) {
++          $object = '';
++          if ( $triple['type'] == 'uri' ) {
++            $object .= '<' . $triple['value'] . '>';
++          } 
++          else {
++            if ( !empty($triple['datatype']) ) {
++              $object .= '"' . $triple['value'] . '"^^' . $triple['datatype'];
++            } 
++            else {
++              $object .= '"' . $this->prepareString($triple['value']) . '"';
++            }
++          }
++          $query .= ' <' . $subject . '> <' . $predicate . '> ' . $object . ' . ';
++        }
++      }
++    }
++    $query .= '}';
++    
++    if ( isset($options['display']) ) {
++      return htmlspecialchars($query);
++    } 
++    else {
++      return $this->executeQuery($query);
++    }
++  }
++  
++  /*
++   * Delete the given URI from the triplestore
++   * 
++   * @param $item
++   *   The rdfx_get_rdf_model() resource
++   * @param $store
++   *   The Virtuoso Store options from the SearchApiServer
++   * @param $graph
++   *   The optional graph URI to update
++   * @param $options
++   *    Array provided to expansion of the function
++   *    Currently, it supports returning the query string
++   */
++  public function deleteItem($item, $store, $graph = FALSE, array $options = array()) {
++    $graph = $graph ? $graph : $store['graph'];
++    $query = 'WITH <' . $graph . '> DELETE { ?s ?p ?o } WHERE { ?s ?p ?o FILTER (?s = <' . $item . '>)}';
++    if ( isset($options['display']) ) {
++      return htmlspecialchars($query);
++    } 
++    else {
++      return $this->executeQuery($query);
++    }
++  }
++  
++  /*
++   * Clear the ARC2 store
++   *
++   * @param $store
++   *   The Virtuoso Store options from the SearchApiServer
++   * @param $graph
++   *   The optional graph URI to update
++   * @param $options
++   *    Array provided to expansion of the function
++   *    Currently, it supports returning the query string
++   */
++  public function clearGraph($store, $graph = FALSE) {
++    $graph = $graph ? $graph : $store['graph'];
++    $query = 'CLEAR GRAPH <' . $graph . '>';
++    if ( isset($options['display']) ) {
++      return htmlspecialchars($query);
++    } 
++    else {
++      return $this->executeQuery($query);
++    }
++  }
++    
++  /*
++   * Connect to the triplestore and run a query
++   *
++   * Virtuoso at the /sparql-auth URL implements Digest Authentication
++   */
++  protected function executeQuery($query) {
++    $url = $this->getTriplestoreUrl() . '?query=' . urlencode($query);
++    
++    if ( !empty($config['query_params']) ) {
++      $url .= $url . '&' . trim($config['query_params']);
++    }
++    
++    $response = drupal_http_request($url, array('headers' => $this->getRequestHeaders()));
++    if ( $response->code == 200 ) {
++      return $response->data;
++    }
++    
++    //Unauthorized
++    elseif ( $response->code == 401 ) {
++    
++      $uri = parse_url($this->getTriplestoreUrl());
++      $this->authenticate($response->headers['www-authenticate'], $uri['path']);
++      
++      $response = drupal_http_request($url,  array('headers' => $this->getRequestHeaders()));
++      if ( $response->code == 200 ) {
++        watchdog('rdf_indexer', "Authenticated with the triplestore at @url.", array('@url' => $this->getTriplestoreUrl()), WATCHDOG_INFO);
++        return $response->data;
++      } 
++      else {
++        watchdog('rdf_indexer', "Could not authenticate with the triplestore while trying to run query: @query.", array('@query' => $query), WATCHDOG_ERROR);
++      }
++    } 
++    else {
++      watchdog('rdf_indexer', "Unknown error[HTTP=@code] occured while trying to run the query: @query.", array('@code' => $response->code, '@query' => $query), WATCHDOG_ERROR);
++    }
++    
++  }
++  
++  /*
++   * Build the Authorization header for Virtuoso
++   *
++   * @param $digest
++   *    The digest information passed from Virtuoso via the WWW-Authenticate header
++   * @param $uri
++   *    The uri path at which the original request was made
++   */
++  protected function authenticate($data, $uri) {
++    $value = explode(' ', $data, 2);
++    $type = $value[0];
++    
++    switch ($type) {
++    
++      case 'Digest':{
++        $digest = array();
++        $parts = explode(", ", $value[1]);
++        foreach ($parts as $element) {
++          $bits = explode("=", $element);
++          $digest[$bits[0]] = str_replace('"', '', $bits[1]);
++        }
++    
++        if ( $digest['qop'] == 'auth') {
++          $cnonce = time();
++          $ncvalue = '00000001';
++          $noncebit = $digest['nonce'] . ':' . $ncvalue . ':' . $cnonce . ':auth:' . md5("GET:" . $uri);
++          $A = md5($this->server->options['username'] . ':' . $digest['realm'] . ':' . $this->server->options['credential']);
++          $respdig = md5("$A:$noncebit");
++          $auth_header = 'Digest username="' . $this->server->options['username'] . '", realm="' . $digest['realm'];
++          $auth_header .= '", nonce="' . $digest['nonce'] . '", uri="' . $uri . '", algorithm=' . $digest['algorithm'];
++          $auth_header .= ', response="' . $respdig . '", opaque="' . $digest['opaque'] . '", qop=' . $digest['qop'];
++          $auth_header .= ', nc=' . $ncvalue . ', cnonce="' . $cnonce . '"';
++      
++          //update the authorization info
++          $config = $this->options;
++          $config['authorization'] = $auth_header;
++          $this->server->options = $config;
++          $this->server->save();
++      
++        } 
++        else {
++          watchdog('rdf_indexer', "Could not authenticate with the triplestore at URI: @uri because the Digest qop != 'auth'. It was '@qop'", array('@uri' => $uri, '@qop' => $digest['qop'] ), WATCHDOG_ERROR);
++        }
++        break;
++      } //end of 'Digest'
++    } //end of switch
++  }
++  
++  /*
++   * Clean up literal string values for processing
++   */
++  protected function prepareString($object = FALSE) {
++    if ( !empty($object) ) {
++      //strip out control characters
++      if ( !ctype_print($object) ) {
++        $object = preg_replace('/[\x00-\x1F\x80-\xFF]/', '', $object);
++      }
++      //escape double quotes
++      $object = str_replace( "\"", "\\\"", $object );
++    }
++    return $object;
++  }
++
++  /*
++   * Set a drupal message with the insert/delete queries 
++   * or the response data for a given entity type and entity ID
++   *
++   * @param $entity_type
++   *    The type of entity to test
++   *
++   * @param $id
++   *    The entity ID
++   *
++   * @param $display
++   *    A boolean flag that if TRUE just displays the queries. 
++   *    If FALSE, the queries are executed and the response data 
++   *    is displayed instead
++   *
++   *  @param $toggle
++   *    A string where 'insert' runs just the insert routine,
++   *    'delete' runs just the delete routine, and
++   *    'both' runs both insert and delete routines
++   */
++  public function testQueries($entity_type, $id, $display = TRUE, $toggle = 'both') {
++    $options = $display ? array('display' => $display) : array();
++    $rdf= rdfx_get_rdf_model($entity_type, $id);
++    
++    $messages = array();
++    if ( $toggle == 'both' || $toggle == 'insert') {
++      $messages['insert'] = $this->indexItem($rdf, $this->options, FALSE, $options);
++    }
++    if ( $toggle == 'both' || $toggle == 'delete') {
++      $messages['delete'] = $this->deleteItem($rdf->uri, $this->options, FALSE, $options);
++    }
++    return $messages;
++  }
++}
+\ No newline at end of file
diff --git a/rdf_indexer.info b/rdf_indexer.info
index a904e7f..38739dc 100644
--- a/rdf_indexer.info
+++ b/rdf_indexer.info
@@ -6,5 +6,8 @@ dependencies[] = search_api
 core = 7.x
 package = RDF
 
+configure = admin/config/search/search_api
+
 files[] = service.inc
 files[] = includes/callback_entity_public.inc
+files[] = service.virtuoso.inc
diff --git a/rdf_indexer.module b/rdf_indexer.module
index b3ed750..f492b85 100644
--- a/rdf_indexer.module
+++ b/rdf_indexer.module
@@ -13,6 +13,11 @@ function rdf_indexer_search_api_service_info() {
     'description' => t('<p>Index items using an RDF store server.</p>'),
     'class' => 'RdfIndexerArc2StoreService',
   );
+  $services['rdf_virtuoso_service'] = array(
+    'name' => t('RDF indexer service for Virtuoso'),
+    'description' => t('<p>Index items using the Virtuoso triplestore.</p>'),
+    'class' => 'RdfIndexerVirtuosoService',
+  );
   return $services;
 }
 
diff --git a/service.inc b/service.inc
index f0d5b21..70c117d 100644
--- a/service.inc
+++ b/service.inc
@@ -3,129 +3,32 @@
 /**
  * Search service class using an RDF store server.
  */
-class RdfIndexerArc2StoreService extends SearchApiAbstractService {
+class RdfIndexerBaseService extends SearchApiAbstractService {
 
   public function __construct(SearchApiServer $server) {
     parent::__construct($server);
   }
-
-  /**
-   * Form callback. Might be called on an uninitialized object - in this case,
-   * the form is for configuring a newly created server.
-   *
-   * Returns an empty form by default.
-   *
-   * @return array
-   *   A form array for setting service-specific options.
+  
+  /*
+   * Return a representation of the triplestore
    */
-  public function configurationForm(array $form, array &$form_state) {
-    ctools_include('export');
-    if (module_exists('arc2_store')) {
-      foreach (ctools_export_crud_load_all('arc2_store_settings') as $store) {
-        if (empty($store->disabled)) {
-          $options[$store->store_id] = $store->label . ' (' . $store->store_id . ')';
-        }
-      }
-    }
-
-    if (empty($options)) {
-      $form['store_id'] = array(
-        '#markup' => t('No ARC2 store found. Please install the ARC2 store module and create some stores.'),
-      );
-    }
-    else {
-      $form['store_id'] = array(
-        '#type' => 'select',
-        '#title' => t('ARC2 store'),
-        '#description' => t('The ARC2 store where the data should be indexed.'),
-        '#options' => $options,
-        '#default_value' => $this->options['store_id'],
-        '#required' => TRUE,
-      );
-    }
-
-    return $form;
-  }
-
-  /**
-   * View this server's settings. Output can be HTML or a render array, a <dl>
-   * listing all relevant settings is preferred.
-   *
-   * The default implementation does a crude output as a definition list, with
-   * option names taken from the configuration form.
-   */
-  public function viewSettings() {
-    $output = '';
-    $store = arc2_store_settings_load($this->options['store_id']);
-    if (!empty($store->settings['endpoint_enabled'])) {
-      $url = url($store->settings['endpoint_path'], array('absolute' => TRUE));
-      $output .= "<dl>\n  <dt>";
-      $output .= t('SPARQL endpoint');
-      $output .= "</dt>\n  <dd>";
-      $output .= l($url, $url);
-      $output .= '</dd>';
-      $output .= "\n</dl>";
-    }
-
-    return $output;
+  public function getStore(SearchApiIndex $index) {
+    return FALSE;
   }
-
-  /**
-   * Notifies this server that it is about to be deleted from the database and
-   * should therefore clean up, if appropriate.
-   *
-   * Note that you shouldn't call the server's save() method, or any
-   * methods that might do that, from inside of this method as the server isn't
-   * present in the database anymore at this point.
-   *
-   * By default, deletes all indexes from this server.
+  /*
+   * Index an item to the given store
    */
-  public function preDelete() {
-    // Only react on real deletes, not on reverts.
-    // @see https://drupal.org/node/1414078
-    // This method could be removed once the above issue is fixed.
-    if ($this->server->hasStatus(ENTITY_IN_CODE)) {
-      return;
-    }
-    $indexes = search_api_index_load_multiple(FALSE, array('server' => $this->server->machine_name));
-    foreach ($indexes as $index) {
-      $this->removeIndex($index);
-    }
-  }
-
-  /**
-   * Add a new index to this server.
-   *
-   * @param SearchApiIndex $index
-   *   The index to add.
+  public function indexItem($item, $store, $graph = FALSE) {}
+  /*
+   * Delete an item from the given store
    */
-  public function addIndex(SearchApiIndex $index) {
-    if (module_exists('search_api_multi') && module_exists('search_api_views')) {
-      views_invalidate_cache();
-    }
-  }
-
-  /**
-   * Notify the server that the indexed field settings for the index have
-   * changed.
-   * If any user action is necessary as a result of this, the method should
-   * use drupal_set_message() to notify the user.
-   *
-   * @param SearchApiIndex $index
-   *   The updated index.
-   *
-   * @return
-   *   TRUE, if this change affected the server in any way that forces it to
-   *   re-index the content. FALSE otherwise.
+  public function deleteItem($item, $store, $graph = FALSE) {}
+  /*
+   * Clear a graph from the given store
    */
-  public function fieldsUpdated(SearchApiIndex $index) {
-    if (module_exists('search_api_multi') && module_exists('search_api_views')) {
-      views_invalidate_cache();
-    }
-    return TRUE;
-  }
-
-  /**
+  public function clearGraph($store, $graph = FALSE) {}
+  
+    /**
    * Index the specified items.
    *
    * @param SearchApiIndex $index
@@ -155,8 +58,8 @@ class RdfIndexerArc2StoreService extends SearchApiAbstractService {
    *   If indexing was prevented by a fundamental configuration error.
    */
   public function indexItems(SearchApiIndex $index, array $items) {
-    // Loads ARC2 store this index is connected to.
-    $store = arc2_store_get_store($index->server()->options['store_id']);
+    
+    $store = $this->getStore($index);
 
     // @todo use documents and merge them before sending to ARC2.
     // $documents = array();
@@ -166,7 +69,7 @@ class RdfIndexerArc2StoreService extends SearchApiAbstractService {
       try {
         // Builds an RDF resource for the entity.
         $rdf = rdfx_get_rdf_model($index->item_type, $id);
-        $store->insert($rdf->index, $rdf->uri);
+        $this->indexItem($rdf, $store);
         $ret[] = $id;
       }
       catch (Exception $e) {
@@ -175,7 +78,7 @@ class RdfIndexerArc2StoreService extends SearchApiAbstractService {
     }
     return $ret;
   }
-
+  
   /**
    * Delete items from an index on this server.
    *
@@ -192,30 +95,199 @@ class RdfIndexerArc2StoreService extends SearchApiAbstractService {
    *   this server should be cleared (then, $ids has to be 'all').
    */
   public function deleteItems($ids = 'all', SearchApiIndex $index = NULL) {
-    // Loads ARC2 store this index is connected to.
-    $store = arc2_store_get_store($index->server()->options['store_id']);
-
+  
+    $store = $this->getStore($index);
+    
     try {
       // Emtpy the local store.
       if ($ids === 'all') {
-        $store->reset();
+        $this->clearGraph($store);
       }
       elseif (is_array($ids)) {
         // Contructs the URI of the graph for each entity ID and deletes it.
-        foreach($ids as $id) {
+        foreach ($ids as $id) {
           $entity = entity_load_single($index->item_type, $id);
-          $uri = rdfx_resource_uri($index->item_type, $entity);
-          $store->delete('', $uri);
+          $uri = rdfx_resource_uri($index->item_type, $id);
+          $this->deleteItem($uri, $store);
         }
       }
     }
-    catch(Exception $e) {
+    catch (Exception $e) {
       watchdog_exception('rdf_indexer', $e, '%type while deleting items from server @server: !message in %function (line %line of %file).', array('@server' => $this->server->name));
     }
   }
 
+  
+  /**
+   * Notifies this server that it is about to be deleted from the database and
+   * should therefore clean up, if appropriate.
+   *
+   * Note that you shouldn't call the server's save() method, or any
+   * methods that might do that, from inside of this method as the server isn't
+   * present in the database anymore at this point.
+   *
+   * By default, deletes all indexes from this server.
+   */
+  public function preDelete() {
+    // Only react on real deletes, not on reverts.
+    // @see https://drupal.org/node/1414078
+    // This method could be removed once the above issue is fixed.
+    if ($this->server->hasStatus(ENTITY_IN_CODE)) {
+      return;
+    }
+    $indexes = search_api_index_load_multiple(FALSE, array('server' => $this->server->machine_name));
+    foreach ($indexes as $index) {
+      $this->removeIndex($index);
+    }
+  }
+  
+  /**
+   * Add a new index to this server.
+   *
+   * @param SearchApiIndex $index
+   *   The index to add.
+   */
+  public function addIndex(SearchApiIndex $index) {
+    if (module_exists('search_api_multi') && module_exists('search_api_views')) {
+      views_invalidate_cache();
+    }
+  }
+  
+  /**
+   * Notify the server that the indexed field settings for the index have
+   * changed.
+   * If any user action is necessary as a result of this, the method should
+   * use drupal_set_message() to notify the user.
+   *
+   * @param SearchApiIndex $index
+   *   The updated index.
+   *
+   * @return
+   *   TRUE, if this change affected the server in any way that forces it to
+   *   re-index the content. FALSE otherwise.
+   */
+  public function fieldsUpdated(SearchApiIndex $index) {
+    if (module_exists('search_api_multi') && module_exists('search_api_views')) {
+      views_invalidate_cache();
+    }
+    return TRUE;
+  }
+  
   public function search(SearchApiQueryInterface $query) {
     throw new SearchApiException(t('The RDF indexer service does not support search. Please query the SPARQL endpoint directly if the RDF store provide such service.'));
   }
+}
+
+/**
+ * Search service class using an RDF store server.
+ */
+class RdfIndexerArc2StoreService extends RdfIndexerBaseService {
+
+  /*
+   * Loads ARC2 store this index is connected to.
+   */
+  public function getStore(SearchApiIndex $index) {
+    return arc2_store_get_store($index->server()->options['store_id']);
+  }
 
+  /**
+   * Form callback. Might be called on an uninitialized object - in this case,
+   * the form is for configuring a newly created server.
+   *
+   * Returns an empty form by default.
+   *
+   * @return array
+   *   A form array for setting service-specific options.
+   */
+  public function configurationForm(array $form, array &$form_state) {
+    ctools_include('export');
+    if (module_exists('arc2_store')) {
+      foreach (ctools_export_crud_load_all('arc2_store_settings') as $store) {
+        if (empty($store->disabled)) {
+          $options[$store->store_id] = $store->label . ' (' . $store->store_id . ')';
+        }
+      }
+    }
+
+    if (empty($options)) {
+      $form['store_id'] = array(
+        '#markup' => t('No ARC2 store found. Please install the ARC2 store module and create some stores.'),
+      );
+    }
+    else {
+      $form['store_id'] = array(
+        '#type' => 'select',
+        '#title' => t('ARC2 store'),
+        '#description' => t('The ARC2 store where the data should be indexed.'),
+        '#options' => $options,
+        '#default_value' => $this->options['store_id'],
+        '#required' => TRUE,
+      );
+    }
+    return $form;
+  }
+
+  /**
+   * View this server's settings. Output can be HTML or a render array, a <dl>
+   * listing all relevant settings is preferred.
+   *
+   * The default implementation does a crude output as a definition list, with
+   * option names taken from the configuration form.
+   */
+  public function viewSettings() {
+    $output = '';
+    $store = arc2_store_settings_load($this->options['store_id']);
+    if (!empty($store->settings['endpoint_enabled'])) {
+      $url = url($store->settings['endpoint_path'], array('absolute' => TRUE));
+      $output .= "<dl>\n  <dt>";
+      $output .= t('SPARQL endpoint');
+      $output .= "</dt>\n  <dd>";
+      $output .= l($url, $url);
+      $output .= '</dd>';
+      $output .= "\n</dl>";
+    }
+
+    return $output;
+  }
+  
+  /*
+   * Index an item to the given store
+   *
+   * @param $item
+   *   The rdfx_get_rdf_model() resource
+   * @param $store
+   *   The ARC2 Store
+   * @param $graph
+   *   The optional graph URI to update (unused for ARC2)
+   */
+  public function indexItem($item, $store, $graph = FALSE) {
+    $store->insert($item->index, $item->uri);
+  }
+  
+  /*
+   * Delete an item from the given store
+   *
+   * @param $item
+   *   The rdfx_get_rdf_model() resource
+   * @param $store
+   *   The ARC2 Store
+   * @param $graph
+   *   The optional graph URI to update (unused for ARC2)
+   */
+  public function deleteItem($item, $store, $graph = FALSE) {
+    $store->delete('', $item);
+  }
+  
+  /*
+   * Clear the ARC2 store
+   *
+   * @param $store
+   *   The ARC2 Store
+   * @param $graph
+   *   The optional graph URI to update (unused for ARC2)
+   */
+  public function clearGraph($store, $graph = FALSE) {
+    $store->reset();
+  }
+  
 }
diff --git a/service.virtuoso.inc b/service.virtuoso.inc
new file mode 100644
index 0000000..4bb4614
--- /dev/null
+++ b/service.virtuoso.inc
@@ -0,0 +1,392 @@
+<?php
+
+/**
+ * Search service class using an RDF store server.
+ */
+class RdfIndexerVirtuosoService extends RdfIndexerBaseService {
+  
+  /*
+   * The external store
+   */
+  public function getStore(SearchApiIndex $index) {
+    return $this->options;
+  }
+  
+  public function getTriplestoreUrl() {
+    if ( !isset($this->options['url']) ) {
+      watchdog('rdf_indexer', "The URL of the Virtuoso Server has not been specified.", WATCHDOG_WARNING);
+    }
+    
+    return $this->options['url'];
+  }
+  
+  public function getRequestHeaders() {
+    $headers = array(
+      'Content-Type' => 'application/x-www-form-urlencoded',
+    );
+    if ( !empty($this->options['authorization']) ) {
+      $headers['Authorization'] = $this->options['authorization'];
+    }
+    return $headers;
+  }
+
+  /**
+   * Form callback. Might be called on an uninitialized object - in this case,
+   * the form is for configuring a newly created server.
+   *
+   * Returns an empty form by default.
+   *
+   * @return array
+   *   A form array for setting service-specific options.
+   */
+  public function configurationForm(array $form, array &$form_state) {
+    global $base_url;
+    
+    $settings = isset($this->options) ? $this->options : array();
+    $form['url'] = array(
+      '#type' => 'textfield',
+      '#title' => t('Virtuoso SPARQL Endpoint URL'),
+      '#description' => t('The Virtuoso SPARQL Endpoint URL where data will be inserted and deleted via basic authentication. Typically, http://example.com:8890/sparql-auth'),
+      '#default_value' => isset($settings['url']) ? $settings['url'] : '',
+      '#required' => TRUE,
+    );
+    $form['graph'] = array(
+      '#type' => 'textfield',
+      '#title' => t('Graph URI'),
+      '#description' => t('The URI of the graph where data will be inserted and deleted.'),
+      '#default_value' => isset($settings['graph']) ? $settings['graph'] : $base_url,
+      '#required' => TRUE,
+    );
+    $form['test_query'] = array(
+      '#type' => 'textfield',
+      '#title' => t('Test SPARQL Query'),
+      '#description' => t('A test query that can be used to verify a connection can be established.'),
+      '#default_value' => isset($settings['test_query']) ? $settings['test_query'] : 'SELECT ?s WHERE {?s ?p ?o} LIMIT 1',
+      '#required' => TRUE,
+    );
+    $form['query_params'] = array(
+      '#type' => 'textfield',
+      '#title' => t('Additional Query Parameters'),
+      '#description' => t('Specify other query parameters that should be added to SPARQL Update requests.'),
+      '#default_value' => isset($settings['query_params']) ? $settings['query_params'] : 'default-graph-uri=&format=text%2Fhtml&timeout=0&debug=on',
+    );
+    $form['username'] = array(
+      '#type' => 'textfield',
+      '#title' => t('Username'),
+      '#description' => t('The username of the Virtuoso account with SPARQL/Update privileges'),
+      '#default_value' => isset($settings['username']) ? $settings['username'] : '',
+      '#required' => TRUE,
+    );   
+    //assume that 'Administer Search API' permission is valid enough to view the password 
+    $form['credential'] = array(
+      '#type' => 'textfield',
+      '#title' => t('Credential'),
+      '#description' => t('The credential of the Virtuoso account with SPARQL/Update privileges'),
+      '#default_value' => isset($settings['credential']) ? $settings['credential'] : '',
+      '#required' => TRUE,
+    ); 
+    $form['authorization'] = array(
+      '#type' => 'hidden',
+      '#value' => isset($settings['authorization']) ? $settings['authorization'] : '',
+    );    
+    return $form;
+  }
+
+  /**
+   * View this server's settings. Output can be HTML or a render array, a <dl>
+   * listing all relevant settings is preferred.
+   *
+   * The default implementation does a crude output as a definition list, with
+   * option names taken from the configuration form.
+   */
+  public function viewSettings() {
+    $settings = isset($this->options) ? $this->options : array();
+    if ( !empty($settings) ) {
+      require './includes/password.inc';
+      $output = "<dl>\n  <dt>";
+      $output .= t('Virtuoso SPARQL endpoint');
+      $output .= "</dt>\n  <dd>";
+      $output .= l($settings['url'], $settings['url']);
+      $output .= "</dd>\n  <dt>";
+      $output .= t('Graph URI');
+      $output .= "</dt>\n<dd>";
+      $output .= $settings['graph'];
+      $output .= "</dd>\n  <dt>";
+      $output .= t('Test Query');
+      $output .= "</dt>\n<dd>";
+      $output .= $settings['test_query'];
+      $output .= "</dd>\n  <dt>";
+      $output .= t('Additional Query Parameters');
+      $output .= "</dt>\n<dd>";
+      $output .= $settings['query_params'];
+      $output .= "</dd>\n  <dt>";
+      $output .= t('Username');
+      $output .= "</dt>\n  <dd>";
+      $output .= $settings['username'];
+      $output .= "</dd>\n  <dt>";
+      $output .= t('Credential');
+      $output .= "</dt>\n <dd>";
+      $output .= user_hash_password($settings['credential']);
+      $output .= "</dd>\n</dl>";
+      return $output;
+    } 
+    else {
+      return 'Virtuoso has not been configured.';
+    }
+  }
+  
+  /*
+   * Index an item to the given store
+   *
+   * @param $item
+   *   The rdfx_get_rdf_model() resource
+   * @param $store
+   *   The Virtuoso Store options from the SearchApiServer
+   * @param $graph
+   *   The optional graph URI to update
+   * @param $options
+   *    Array provided to expansion of the function
+   *    Currently, it supports returning the query string
+   */
+  public function indexItem($item, $store, $graph = FALSE, $options = array()) {
+    $graph =  $this->server->options['graph'];
+    $beginning = 'INSERT DATA INTO GRAPH <' . $graph . '> { ';
+    $end = ' } ';
+    $query = '';
+    $queries = array();
+    $triple_count = 0;
+    foreach($item->index as $subject => $triples){
+      //clear out the item in case it already exists 
+      $this->deleteItem($subject, $store);
+      foreach($triples as $predicate => $value){
+        foreach($value as $triple){
+          $object = '';
+          if( $triple['type'] == 'uri' ){
+            $object .= '<'.$triple['value'].'>';
+          } else {
+            if( !empty($triple['datatype']) ){
+              $object .= '"'.$this->prepareString($triple['value']).'"^^'.$triple['datatype'];
+            } else {
+              $object .= '"'.$this->prepareString($triple['value']).'"';
+            }
+          }
+          $query .= ' <'.$subject.'> <'.$predicate.'> '.$object.' . ';
+          $triple_count += 1;
+          //this is to avoid  ERROR:
+          //Virtuoso 37000 Error SP031: SPARQL: Internal error: The length of generated SQL text has exceeded 10000 lines of code
+          if( $triple_count == 500 ){
+            $queries[] = $beginning . $query . $end;
+            $triple_count = 0;
+            $query = '';
+          }
+        }
+      }
+    }
+    if( strlen($query) > 0 ){
+      $queries[] = $beginning . $query . $end;
+    }
+
+    if( isset($options['display']) ){
+      $query = '';
+      foreach($queries as $q){
+        $query .= $q;
+      }
+      return htmlspecialchars($query);
+    } else {
+      $response = '';
+      foreach($queries as $query){
+        $response .= $this->executeQuery($query);
+      }
+      return $response;
+    }
+  }
+  
+  /**
+   * Delete the given URI from the triplestore
+   * 
+   * @param $item
+   *   The rdfx_get_rdf_model() resource
+   * @param $store
+   *   The Virtuoso Store options from the SearchApiServer
+   * @param $graph
+   *   The optional graph URI to update
+   * @param $options
+   *    Array provided to expansion of the function
+   *    Currently, it supports returning the query string
+   */
+  public function deleteItem($item, $store, $graph = FALSE, array $options = array()) {
+    $graph = $graph ? $graph : $store['graph'];
+    $query = 'WITH <' . $graph . '> DELETE { ?s ?p ?o } WHERE { ?s ?p ?o FILTER (?s = <' . $item . '>)}';
+    if ( isset($options['display']) ) {
+      return htmlspecialchars($query);
+    } 
+    else {
+      return $this->executeQuery($query);
+    }
+  }
+  
+  /*
+   * Clear the Virtuoso store of all values from this index
+   * *** NEEDS UPDATING TO GET ALL ENTITIES FOR THIS INDEX
+   *     AND REMOVE ONLY THOSE ***
+   *
+   * @param $store
+   *   The Virtuoso Store options from the SearchApiServer
+   * @param $graph
+   *   The optional graph URI to update
+   * @param $options
+   *    Array provided to expansion of the function
+   *    Currently, it supports returning the query string
+   */
+  public function clearGraph($store, $graph = FALSE) {
+    $graph = $graph ? $graph : $store['graph'];
+    $query = 'CLEAR GRAPH <' . $graph . '>';
+    if ( isset($options['display']) ) {
+      return htmlspecialchars($query);
+    } 
+    else {
+      return $this->executeQuery($query);
+    }
+  }
+    
+  /*
+   * Connect to the triplestore and run a query
+   *
+   * Virtuoso at the /sparql-auth URL implements Digest Authentication
+   */
+  protected function executeQuery($query) {
+    $url = $this->getTriplestoreUrl();
+    $config = $this->options;
+ 
+    $data = 'query=' . urlencode($query);
+    if( !empty($config['query_params']) ){
+      $data .= '&' . $config['query_params'];
+    } 
+    $content = array(
+      'method' => 'POST',
+      'headers' => $this->getRequestHeaders(),
+      'data' => $data,
+    ); 
+    $response = drupal_http_request($url, $content);
+    if( $response->code == 200 ) {
+      return $response->data;
+    }
+    
+    //Unauthorized
+    elseif ( $response->code == 401 ) {
+    
+      $uri = parse_url($this->getTriplestoreUrl());
+      $this->authenticate($response->headers['www-authenticate'], $uri['path']);
+      //the authenticate method above will have added the new auth headers 
+      $content['headers'] = $this->getRequestHeaders(); 
+      $response = drupal_http_request($url, $content);
+      if ( $response->code == 200 ) {
+        watchdog('rdf_indexer', "@data.", array('@data' => $response->data), WATCHDOG_INFO);
+        return $response->data;
+      } 
+      else {
+        watchdog('rdf_indexer', "Could not authenticate with the triplestore while trying to run query: @query", array('@query' => $query), WATCHDOG_ERROR);
+      }
+    } 
+    else {
+      watchdog('rdf_indexer', "Unknown error[HTTP=@code] occured while trying to run the query: @query", array('@code' => $response->code, '@query' => $query), WATCHDOG_ERROR);
+    }
+  }
+  
+  /*
+   * Build the Authorization header for Virtuoso
+   *
+   * @param $digest
+   *    The digest information passed from Virtuoso via the WWW-Authenticate header
+   * @param $uri
+   *    The uri path at which the original request was made
+   */
+  protected function authenticate($data, $uri) {
+    $value = explode(' ', $data, 2);
+    $type = $value[0];
+    
+    switch ($type) {
+    
+      case 'Digest':{
+        $digest = array();
+        $parts = explode(", ", $value[1]);
+        foreach ($parts as $element) {
+          $bits = explode("=", $element);
+          $digest[$bits[0]] = str_replace('"', '', $bits[1]);
+        }
+    
+        if ( $digest['qop'] == 'auth') {
+          $cnonce = time();
+          $ncvalue = '00000001';
+          $noncebit = $digest['nonce'] . ':' . $ncvalue . ':' . $cnonce . ':auth:' . md5("POST:" . $uri);
+          $A = md5($this->server->options['username'] . ':' . $digest['realm'] . ':' . $this->server->options['credential']);
+          $respdig = md5("$A:$noncebit");
+          $auth_header = 'Digest username="' . $this->server->options['username'] . '", realm="' . $digest['realm'];
+          $auth_header .= '", nonce="' . $digest['nonce'] . '", uri="' . $uri . '", algorithm=' . $digest['algorithm'];
+          $auth_header .= ', response="' . $respdig . '", opaque="' . $digest['opaque'] . '", qop=' . $digest['qop'];
+          $auth_header .= ', nc=' . $ncvalue . ', cnonce="' . $cnonce . '"';
+      
+          //update the authorization info
+          $config = $this->options;
+          $config['authorization'] = $auth_header;
+          $this->server->options = $config;
+          $this->server->save();
+      
+        } 
+        else {
+          watchdog('rdf_indexer', "Could not authenticate with the triplestore at URI: @uri because the Digest qop != 'auth'. It was '@qop'", array('@uri' => $uri, '@qop' => $digest['qop'] ), WATCHDOG_ERROR);
+        }
+        break;
+      } //end of 'Digest'
+    } //end of switch
+  }
+  
+  /*
+   * Clean up literal string values for processing
+   */
+  protected function prepareString($object = FALSE) {
+    if ( !empty($object) ) {
+      //strip out control characters
+      if ( !ctype_print($object) ) {
+        $object = preg_replace('/[\x00-\x1F\x80-\xFF]/', '', $object);
+      }
+      //escape double quotes
+      $object = str_replace( "\"", "\\\"", $object );
+    }
+    return $object;
+  }
+
+  /*
+   * Set a drupal message with the insert/delete queries 
+   * or the response data for a given entity type and entity ID
+   *
+   * @param $entity_type
+   *    The type of entity to test
+   *
+   * @param $id
+   *    The entity ID
+   *
+   * @param $display
+   *    A boolean flag that if TRUE just displays the queries. 
+   *    If FALSE, the queries are executed and the response data 
+   *    is displayed instead
+   *
+   *  @param $toggle
+   *    A string where 'insert' runs just the insert routine,
+   *    'delete' runs just the delete routine, and
+   *    'both' runs both insert and delete routines
+   */
+  public function testQueries($entity_type, $id, $display = TRUE, $toggle = 'both') {
+    $options = $display ? array('display' => $display) : array();
+    $rdf= rdfx_get_rdf_model($entity_type, $id);
+    
+    $messages = array();
+    if ( $toggle == 'both' || $toggle == 'insert') {
+      $messages['insert'] = $this->indexItem($rdf, $this->options, FALSE, $options);
+    }
+    if ( $toggle == 'both' || $toggle == 'delete') {
+      $messages['delete'] = $this->deleteItem($rdf->uri, $this->options, FALSE, $options);
+    }
+    return $messages;
+  }
+}
