commit 83dc60b9d288d64fe69247a44814e50887f9dd8f
Author: Andreas Kohlbecker <a.kohlbecker@bgbm.org>
Date:   Mon Sep 3 18:44:48 2018 +0200

    ref #7658 adapting cdm_taxonRelationships() to new taxon relationship DTO webservice
     - test case
     - extending tagged text functions
     - adapting misapplied name redering

diff --git a/modules/cdm_dataportal/cdm_api/cdm_api.module b/modules/cdm_dataportal/cdm_api/cdm_api.module
index 2535554e..78e1e473 100644
--- a/modules/cdm_dataportal/cdm_api/cdm_api.module
+++ b/modules/cdm_dataportal/cdm_api/cdm_api.module
@@ -192,10 +192,17 @@ function cdm_api_cron() {
 
 // ===================== Tagged Text functions ================== //
 
+function tagged_text_new($tag_type, $text = null){
+  $tt = new stdClass();
+  $tt->type = $tag_type;
+  $tt->text = $text;
+  return $tt;
+}
+
 /**
  * Walks the passed TaggedText array to find all elements which have a
  * TaggedText->entityReference. For each of these the taggedTexts is loaded
- * from the webservice and the original enty in the TaggedText array will be
+ * from the webservice and the original entry in the TaggedText array will be
  * replaced by the newly loaded array.
  *
  * @param array $taggedtxt
@@ -234,13 +241,17 @@ function cdm_tagged_text_expand_entity_references(array $taggedtxt, $skiptags =
  * Each item is provided with a class attribute which is set to the key of the
  * TaggedText item.
  *
+ * Tagged text where the type starts with 'PLACEHOLDER_' will be added to the markup as plain text whereas the
+ * taggedText->type wrapped in curly brackets: '{'. $tt->text . '}' is used as text.
+ * see tagged_text_extract_secref()
+ *
  * @param array $taggedtxt
  *   Array with text items to convert.
  * @param array $skiptags
  *   Array of tag names to skip
  *
  * @return string
- *   A string with HTML.
+ *   The markup.
  */
 function cdm_tagged_text_to_markup(array $taggedtxt, $skiptags = array()) {
 
@@ -258,10 +269,46 @@ function cdm_tagged_text_to_markup(array $taggedtxt, $skiptags = array()) {
       $is_last = $i + 1 == count($taggedtxt);
       $is_separator = is_tagged_text_sepatator_type($tt->type);
       $glue = !$is_separator && !$was_separator && !$is_last ? ' ' : '';
-      $out .= '<' . $tag . ' class="' . $class_attr . '">'
-        . t('@text', array('@text' => $tt->text))
-        . '</' . $tag . '>'
-        . $glue;
+      if(str_beginsWith($tt->type, 'PLACEHOLDER_')){
+        $out .=  '{'. $tt->type . '}';
+      } else {
+        $out .= '<' . $tag . ' class="' . $class_attr . '">'
+          . t('@text', array('@text' => $tt->text))
+          . '</' . $tag . '>'
+          . $glue;
+        }
+      $was_separator = $is_separator;
+    }
+    $i++;
+  }
+  return $out;
+}
+
+/**
+ * Converts an array of TaggedText items into corresponding plain text string
+ *
+ * Each item is provided with a class attribute which is set to the key of the
+ * TaggedText item.
+ *
+ * @param array $taggedtxt
+ *   Array with text items to convert.
+ * @param array $skiptags
+ *   Array of tag names to skip
+ *
+ * @return string
+ *   The plain text
+ */
+function cdm_tagged_text_to_string(array $taggedtxt, $skiptags = array()) {
+
+  $out = '';
+  $was_separator = false;
+  $i = 0;
+  foreach ($taggedtxt as $tt) {
+    if (!in_array($tt->type, $skiptags) && $tt->text) {
+      $is_last = $i + 1 == count($taggedtxt);
+      $is_separator = is_tagged_text_sepatator_type($tt->type);
+      $glue = !$is_separator && !$was_separator && !$is_last ? ' ' : '';
+      $out .= t('@text', array('@text' => $tt->text)) . $glue;
       $was_separator = $is_separator;
     }
     $i++;
@@ -351,30 +398,50 @@ function normalize_tagged_text(&$taggedTextList) {
   }
 }
 
-function split_secref_from_tagged_text(&$tagged_text) {
+/**
+ * Extracts the tagged text for sec references with separator and citation detail from a tagged text array.
+ * @param $tagged_text
+ *    The tagged text to operate on
+ * @param string $ref_tag_type
+ *    The tagtype for a secreference is "secReference", but "relSecReference" is also used in case of relationships.
+ * @param bool $replace_with_placeholder
+ *    Indicates the method to add a empty placeholder tagged text alement as relpacement for the extrated tagged text
+ *    elements.
+ * @return array
+ */
+function tagged_text_extract_secref(&$tagged_text, $ref_tag_type = "secReference", $replace_with_placeholder = false) {
 
   $extracted_tt = array();
   if (is_array($tagged_text)) {
+    $extract_pos = null;
     for ($i = 0; $i < count($tagged_text) - 1; $i++) {
-      if ($tagged_text[$i + 1]->type == "secReference" && $tagged_text[$i]->type == "separator"){
+      if ($tagged_text[$i + 1]->type == $ref_tag_type && $tagged_text[$i]->type == "separator"){
         $extracted_tt[0] = $tagged_text[$i];
         $extracted_tt[1] = $tagged_text[$i + 1];
-        unset($tagged_text[$i]);
+
+        if($replace_with_placeholder){
+          // text must not be null, see cdm_tagged_text_to_markup()
+          $tagged_text[$i] = tagged_text_new("PLACEHOLDER_" . $ref_tag_type, "PLACEHOLDER_" . $ref_tag_type);
+        } else {
+          unset($tagged_text[$i]);
+        }
         unset($tagged_text[$i + 1]);
-        // also get the microfererence which could be in $tagged_text[$i + 3]
-        if(isset($tagged_text[$i + 3])  && $tagged_text[$i + 2]->type == "separator" && $tagged_text[$i + 3]->type == "secReference"){
+        // also get the microreference which could be in $tagged_text[$i + 3]
+        if(isset($tagged_text[$i + 3])  && $tagged_text[$i + 2]->type == "separator" && $tagged_text[$i + 3]->type == $ref_tag_type){
           $extracted_tt[2] = $tagged_text[$i + 2];
           $extracted_tt[3] = $tagged_text[$i + 3];
+          unset($tagged_text[$i + 2]);
+          unset($tagged_text[$i + 3]);
         }
         break;
       }
     }
   }
+  $tagged_text = array_values($tagged_text); // re-index array to make it continuous again
   return $extracted_tt;
 }
 
-
-function split_nomstatus_from_tagged_text(&$tagged_text) {
+function tagged_text_extract_nomstatus(&$tagged_text) {
 
   $extracted_tt = array();
   if (is_array($tagged_text)) {
diff --git a/modules/cdm_dataportal/cdm_dataportal.css b/modules/cdm_dataportal/cdm_dataportal.css
index ecb211e1..cbcc2f61 100755
--- a/modules/cdm_dataportal/cdm_dataportal.css
+++ b/modules/cdm_dataportal/cdm_dataportal.css
@@ -323,9 +323,7 @@ ul.typeDesignations li {
 .misapplied .name {
   /* font-style: normal; */
 }
-.misapplied .authors {
-  display: none;
-}
+
 #edit-search-domisappliednames {
   vertical-align:baseline;
 }
diff --git a/modules/cdm_dataportal/includes/name.inc b/modules/cdm_dataportal/includes/name.inc
index 8931adc8..8a8f188d 100644
--- a/modules/cdm_dataportal/includes/name.inc
+++ b/modules/cdm_dataportal/includes/name.inc
@@ -304,8 +304,8 @@ function render_taxon_or_name($taxon_name_or_taxon_base, $nameLink = NULL, $refe
     }
   }
 
-  $secref_tagged_text = split_secref_from_tagged_text($taggedTitle);
-  $nom_status_tagged_text = split_nomstatus_from_tagged_text($taggedTitle);
+  $secref_tagged_text = tagged_text_extract_secref($taggedTitle);
+  $nom_status_tagged_text = tagged_text_extract_nomstatus($taggedTitle);
   $appended_phrase_tagged_text = array(); // this is filled later
 
   normalize_tagged_text($taggedTitle);
diff --git a/modules/cdm_dataportal/includes/taxon.inc b/modules/cdm_dataportal/includes/taxon.inc
index b3b4102a..049139f8 100644
--- a/modules/cdm_dataportal/includes/taxon.inc
+++ b/modules/cdm_dataportal/includes/taxon.inc
@@ -50,27 +50,24 @@ define('TAXON_TYPE_MISAPPLIEDNAME', 'misapplied-name');
  */
 function cdm_taxonRelationships($taxonRelationshipsDTO, $focusedTaxon){
 
-  static $NULL_AUTHORTEAM = 'NULL_AUTHORTEAM';
-
   if (!$taxonRelationshipsDTO || $taxonRelationshipsDTO->size < 1) {
     return null;
   }
 
   RenderHints::pushToRenderStack('taxon_relationships');
-  $footnoteListKey = 'taxon_relationships';
-  RenderHints::setFootnoteListKey($footnoteListKey);
+  $footnote_list_key = 'taxon_relationships';
+  RenderHints::setFootnoteListKey($footnote_list_key);
 
   $misapplied = array();
-  $joinedAuthorTeams = array();
+  $joined_sensu_refs = array();
 
   $taxon_relationship_types = variable_get(CDM_TAXON_RELATIONSHIP_TYPES, unserialize(CDM_TAXON_RELATIONSHIP_TYPES_DEFAULT));
 
   // Aggregate misapplied names having the same fullname:
   //  - deduplicate misapplied names, so that the same name it not shown multiple times in case it
   //    the duplicates only have different sensu references/author teams (see #5647)
-  //  - show the author team as sec reference
-  //  - show the according reference as footnote to this author team
-  //  - if the sec reference has no author team it should show instead the title cache
+  //  - show only the author team as short citation for the sec reference
+  //  - show the according reference as footnote to this short citation
   //
   // Example:
   // "Xenoxylum foobar" sensu Grumbach¹; sensu Lem²
@@ -85,87 +82,112 @@ function cdm_taxonRelationships($taxonRelationshipsDTO, $focusedTaxon){
 
           RenderHints::pushToRenderStack('misapplied_name_for'); // TODO the render path string should in future come from $taxonRelation->type->...
 
-          $name = join(' ', cdm_tagged_text_values($taxon_relation->taggedText, array('name')));
-
-          $sensu_citation_footnote_str = null;
-
-          if (!isset($misapplied[$name])) {
-            // Render the first name found as representative for all others.
-
-            //###OLD: $misapplied[$name]['out'] = cdm_related_taxon($taxon_relation->fromTaxon, UUID_MISAPPLIED_NAME_FOR);
-            $name_tagged_text = array();
-            $secref_tagged_text = null;
-            foreach($taxon_relation->taggedText as $tt){
-              if($tt->type !== 'secReference' && !$secref_tagged_text){
-                $name_tagged_text[] = $tt;
-              } else {
-                if(!$secref_tagged_text){
-                  // initialize $secref_tagged_text
-                  $secref_tagged_text = array();
-                  $secref_tagged_text[] = array_pop($name_tagged_text); // remmove last elemetn wich is a separator in this case
-                }
-                $secref_tagged_text[] = $tt;
+          // using name tags + author as full name key
+          $full_name_key = join(' ', cdm_tagged_text_values($taxon_relation->taggedText, array('name', 'authors')));
+
+          // Render the first name found as representative for all others.
+          //###OLD: $misapplied[$name]['out'] = cdm_related_taxon($taxon_relation->fromTaxon, UUID_MISAPPLIED_NAME_FOR);
+          $sensu_tagged_text = tagged_text_extract_secref($taxon_relation->taggedText, "secReference", true);
+          $relsec_tagged_text = tagged_text_extract_secref($taxon_relation->taggedText, "relSecReference", true);
+
+          if (isset($sensu_tagged_text[1])) {
+            array_shift($sensu_tagged_text); // remove first element which contains the "sensu", this will be added later in this code
+            $sensu_citation_short_markup = cdm_tagged_text_to_markup($sensu_tagged_text);
+            $sensu_citation_short = cdm_tagged_text_to_string($sensu_tagged_text);
+            $sensu_uuid = $sensu_tagged_text[0]->entityReference->uuid;
+            $misapplied[$full_name_key]['sensu_uuids'][] = $sensu_uuid;
+            if (!isset($joined_sensu_refs[$sensu_uuid])) {
+              $joined_sensu_refs[$sensu_uuid] = array(
+                'short_citation' => $sensu_citation_short,
+                'markup' => $sensu_citation_short_markup // to be expanded later as short_citation + footnote key
+              );
+            }
+
+            if (isset($relsec_tagged_text[1])) {
+              array_shift($relsec_tagged_text); // remove first element which contains the "err. sec", this will be added later in this code
+              $relsec_citation_short_markup = cdm_tagged_text_to_markup($relsec_tagged_text);
+              $relsec_citation_short = cdm_tagged_text_to_string($relsec_tagged_text);
+              $relsec_uuid = $relsec_tagged_text[0]->entityReference->uuid;
+              $misapplied[$full_name_key]['relsec_uuids'][] = $relsec_uuid;
+              if (!isset($joined_sensu_refs[$relsec_uuid])) {
+                $joined_sensu_refs[$relsec_uuid] = array(
+                  'short_citation' => $relsec_citation_short,
+                  'markup' => $relsec_citation_short_markup // to be expanded later as short_citation + footnote key
+                );
               }
             }
-            $misapplied[$name]['out'] = cdm_tagged_text_to_markup($name_tagged_text);
-          } else {
-            // We need to add the anchors for all of the other misapplied names not
-            // being rendered explicitly.
-            $misapplied[$name]['out'] = uuid_anchor($taxon_relation->taxonUuid, $misapplied[$name]['out']);
-          }
 
-          // Collect all authors for this fullname.
-          if (isset($authorteam_str) && $authorteam_str != $NULL_AUTHORTEAM) {
-            // prepare entry for the footnote key of the sensu citation footnote
-            $misapplied[$name]['authorteam'][$authorteam_str] = '';
-            // map sensu citation footnote to the authorteam string
-            $joinedAuthorTeams[$authorteam_str] = $sensu_citation_footnote_str;
+            if (!isset($misapplied[$full_name_key]['out'])) {
+              $misapplied[$full_name_key]['out'] = cdm_tagged_text_to_markup($taxon_relation->taggedText);
+            } else {
+              // We need to add the anchors for all of the other misapplied names not
+              // being rendered explicitly.
+              $misapplied[$full_name_key]['out'] = uuid_anchor($taxon_relation->taxonUuid, $misapplied[$full_name_key]['out']);
+            }
+          } else {
+            RenderHints::pushToRenderStack('other_taxon_relationship');
+            // All relationship types except misapplied_name_for and invalid_designation_for.
+            $taxon_relationships_lines[] = cdm_taxonRelationship($taxon_relation, TRUE, _is_invers_taxonRelationship($taxon_relation, $focusedTaxon));
           }
 
-        } else {
-          RenderHints::pushToRenderStack('other_taxon_relationship');
-          // All relationship types except misapplied_name_for and invalid_designation_for.
-          $taxon_relationships_lines[] = cdm_taxonRelationship($taxon_relation, TRUE, _is_invers_taxonRelationship($taxon_relation, $focusedTaxon));
+          RenderHints::popFromRenderStack();
         }
-
-        RenderHints::popFromRenderStack();
       }
-    }
+    } // END loop over $taxonRelationshipsDTO->relations
   }
 
-  // Sort the joinedAuthorTeams and create footnotes and footnotekeys.
-  ksort($joinedAuthorTeams);
-  $author_team_cnt = 0;
-  foreach ($joinedAuthorTeams as $authorteam_str => $sensu_citation) {
-    $footnoteKey = '';
-    if(!empty($sensu_citation)) {
-      $footnoteKey = FootnoteManager::addNewFootnote($footnoteListKey, $sensu_citation);
-      $footnoteKey = theme('cdm_footnote_key', array('footnoteKey' => $footnoteKey));
+  // Sort the $joined_sensu_refs and create footnotes and footnote keys.
+  uasort($joined_sensu_refs, function($a, $b) {
+    if ($a['short_citation'] == $b['short_citation']) {
+      return 0;
     }
-    $sensu = '';
-    if($authorteam_str != $NULL_AUTHORTEAM){
-      $sensu = ++$author_team_cnt == 0 ? '' : 'sensu ' . $authorteam_str;
+    return ($a['short_citation'] < $b['short_citation']) ? -1 : 1;
+  });
+
+
+  foreach ($joined_sensu_refs as $sensu_uuid => $sensu_strings) {
+    $footnote_key = '';
+    if(!empty($sensu_strings)) {
+      $sensu_reference = cdm_ws_get(CDM_WS_REFERENCE, $sensu_uuid);
+      if(!$sensu_reference){
+        drupal_set_message("Problem fetching sensu reference with uuid " . $sensu_uuid, 'error');
+      }
+      $footnote_key = FootnoteManager::addNewFootnote($footnote_list_key, $sensu_reference->titleCache);
+      $footnote_key = theme('cdm_footnote_key', array('footnoteKey' => $footnote_key));
     }
-    $joinedAuthorTeams[$authorteam_str] = '<span class="sensu">' . $sensu . $footnoteKey .'</span>';
+    $joined_sensu_refs[$sensu_uuid]['markup'] = '<span class="sensu">' . $sensu_strings['markup'] . $footnote_key . '</span>';
   }
 
   // ---- Generate output ---- //
-
   $out = '<div class="taxon-relationships">';
   if (is_array($misapplied) && count($misapplied) > 0) {
+    ksort($misapplied); // order the misapplied by scientific name
     $out .= '<ul class="misapplied">';
+
     foreach ($misapplied as $misapplied_name) {
+      $misapplied_name_markup = $misapplied_name['out'];
 
-      $out .= '<li class="synonym"><span class="misapplied">' . $misapplied_name['out'] . ' </span>';
+      if (isset($misapplied_name['sensu_uuids'])) {
+        $sensu_refs_with_fkey = array();
+        foreach ($misapplied_name['sensu_uuids'] as $sensu_uuid) {
+          $sensu_refs_with_fkey[$joined_sensu_refs[$sensu_uuid]['short_citation']] = $joined_sensu_refs[$sensu_uuid]['markup'];
+        }
+        ksort($sensu_refs_with_fkey);
+        $sensu_refs_with_fkey_markup = join('; ', $sensu_refs_with_fkey);
+        $misapplied_name_markup = str_replace('{PLACEHOLDER_secReference}', ' sensu ' . $sensu_refs_with_fkey_markup, $misapplied_name_markup);
+      }
 
-      if (isset($misapplied_name['authorteam'])) {
-        // Fill authors with the renderedFootnoteKey and sorting 'em.
-        foreach ($misapplied_name['authorteam'] as $authorteam_str => &$renderedFootnoteKey) {
-          $renderedFootnoteKey = $joinedAuthorTeams[$authorteam_str];
+      if (isset($misapplied_name['relsec_uuids'])) {
+        $relsec_refs_with_fkey = array();
+        foreach ($misapplied_name['relsec_uuids'] as $relsec_uuid) {
+          $relsec_refs_with_fkey[$joined_sensu_refs[$relsec_uuid]['short_citation']] = $joined_sensu_refs[$relsec_uuid]['markup'];
         }
-        ksort($misapplied_name['authorteam']);
-        $out .= join('; ', $misapplied_name['authorteam']);
+        ksort($relsec_refs_with_fkey);
+        $relsec_refs_with_fkey_markup = join('; ', $relsec_refs_with_fkey);
+        $misapplied_name_markup = str_replace('{PLACEHOLDER_relSecReference}', ', err. sec. ' . $relsec_refs_with_fkey_markup, $misapplied_name_markup);
       }
+
+      $out .= '<li class="synonym"><span class="misapplied">' . $misapplied_name_markup . ' </span>';
       $out .= '</li>';
     }
     $out .= '</ul>';
@@ -179,19 +201,10 @@ function cdm_taxonRelationships($taxonRelationshipsDTO, $focusedTaxon){
     $out .= '</ul>';
   }
 
-  $footnotes = theme('cdm_footnotes', array('footnoteListKey' => $footnoteListKey, 'enclosingTag' => 'li'));
-  $footnotes .= theme('cdm_annotation_footnotes', array('footnoteListKey' => $footnoteListKey, 'enclosingTag' => 'li'));
-
-// AK: why splitting footnotes at the sensu string ??? this is weired and hacky
-//     TODO remove below dead code
-//   $tr_footnotes_exploded = explode('sensu', $tr_footnotes);
-//   $tr_footnotes_aux = '';
-//   foreach ($tr_footnotes_exploded as $element) {
-//     $tr_footnotes_aux .= $element;
-//   }
+  $footnotes = theme('cdm_footnotes', array('footnoteListKey' => $footnote_list_key, 'enclosingTag' => 'li'));
+  $footnotes .= theme('cdm_annotation_footnotes', array('footnoteListKey' => $footnote_list_key, 'enclosingTag' => 'li'));
 
   $out .= '<ul class="footnotes">' . $footnotes . '</ul>';
-
   $out .= '</div>';
 
   RenderHints::popFromRenderStack();
diff --git a/src/test/java/eu/etaxonomy/dataportal/selenium/tests/reference/MisappliedNamesTest.java b/src/test/java/eu/etaxonomy/dataportal/selenium/tests/reference/MisappliedNamesTest.java
index fed7fea6..2108c708 100644
--- a/src/test/java/eu/etaxonomy/dataportal/selenium/tests/reference/MisappliedNamesTest.java
+++ b/src/test/java/eu/etaxonomy/dataportal/selenium/tests/reference/MisappliedNamesTest.java
@@ -101,6 +101,9 @@ public class MisappliedNamesTest extends CdmDataPortalTestBase{
     /**
      * https://dev.e-taxonomy.eu/redmine/issues/7658
      * https://dev.e-taxonomy.eu/redmine/issues/6682
+     *
+     * TODO 1b) Der Buchlink verweist auf den Protolog von N. blancoi Blume – der hat ja aber gerade nichts mit dieser Namensanwendung zu tun (also: Protologbildlinks müssen bei MAN weggelassen werden oder der Link muss nach dem <non Blume>
+eingefügt werden)
      */
     @Test
     @Ignore
