From 2cdc1fc4346984f289c80f15c24543e7130fb75a Mon Sep 17 00:00:00 2001 From: root Date: Mon, 23 Feb 2026 21:10:26 +0100 Subject: [PATCH] Initial commit --- Module.php | 48 ++ README.md | 99 ++++ config/module.config.php | 51 ++ src/BIBBNeo/Module.php | 48 ++ src/BIBBNeo/RecordDriver/BIBBRecordTrait.php | 400 ++++++++++++++++ src/BIBBNeo/RecordDriver/SolrDspace.php | 439 ++++++++++++++++++ .../RecordDriver/SolrDspaceFactory.php | 40 ++ src/BIBBNeo/RecordDriver/SolrMarc.php | 25 + src/BIBBNeo/RecordDriver/SolrMarcFactory.php | 40 ++ 9 files changed, 1190 insertions(+) create mode 100644 Module.php create mode 100644 README.md create mode 100644 config/module.config.php create mode 100644 src/BIBBNeo/Module.php create mode 100644 src/BIBBNeo/RecordDriver/BIBBRecordTrait.php create mode 100644 src/BIBBNeo/RecordDriver/SolrDspace.php create mode 100644 src/BIBBNeo/RecordDriver/SolrDspaceFactory.php create mode 100644 src/BIBBNeo/RecordDriver/SolrMarc.php create mode 100644 src/BIBBNeo/RecordDriver/SolrMarcFactory.php diff --git a/Module.php b/Module.php new file mode 100644 index 0000000..02d56f0 --- /dev/null +++ b/Module.php @@ -0,0 +1,48 @@ + [ + 'namespaces' => [ + __NAMESPACE__ => __DIR__ . '/src/BIBBNeo', + ], + ], + ]; + } +} diff --git a/README.md b/README.md new file mode 100644 index 0000000..8ad3cc7 --- /dev/null +++ b/README.md @@ -0,0 +1,99 @@ +# BIBBNeo Module für VuFind 10.2 + +Eigenes VuFind-Modul für das Bundesinstitut für Berufsbildung (BIBB). +Ersetzt die bisherigen Core-Modifikationen durch saubere Overrides. + +## Architektur + +``` +VuFind\RecordDriver\SolrDefault (Core 10.2, unangetastet) + │ + ├── VuFind\RecordDriver\SolrMarc (Core 10.2) + │ │ + │ └── BIBBNeo\RecordDriver\SolrMarc + │ BIBB-Felder + geänderte Core-Methoden (via Trait) + │ Für: Koha-MARC-Records (recordtype = "marc") + │ + └── BIBBNeo\RecordDriver\SolrDspace + DSpace-spezifische Methoden + BIBB-Felder (via Trait) + Für: DSpace-Records (recordtype = "dspace") +``` + +### BIBBRecordTrait + +Gemeinsamer PHP-Trait, der von beiden RecordDrivern verwendet wird: + +- **BIBB-Solr-Felder**: citation, citationdr, extend, participation, + researchfocus, accompanyingmaterial, additionallinks, dissertationnote, + originalversion, reviewof, replaces, isreplacedby, classification, + voevzlink, ctrlnum +- **Core-Overrides**: getAllSubjectHeadings (nur topic), getFormats + (medium vor format), getSeries (series + series2), getContainerVolume + (Array-Handling), getSupportedCitationFormats (BIBB, BIBBBWP, APA) + +### SolrDspace (zusätzlich zum Trait) + +- Bitstream-URL-Handling mit VOE-VZ-Link-Zuordnung +- DOI/URN-Auflösung +- DSpace-Handle-Filterung +- Rechte/Embargo-Felder (license, rightslicense, rightsuri, etc.) +- getOnlineLinks(), getPids(), getFormatsFiltered() + +## Installation + +### 1. Modul kopieren + +```bash +cp -r module/BIBBNeo /usr/local/vufind/module/ +``` + +### 2. Modul aktivieren + +In `/usr/local/vufind/local/httpd-vufind.conf` (oder `env.d/`) die +Umgebungsvariable VUFIND_LOCAL_MODULES ergänzen: + +```apache +SetEnv VUFIND_LOCAL_MODULES "BIBBNeo" +``` + +Alternativ in `/usr/local/vufind/local/config/vufind/config.ini`: + +```ini +[System] +; Falls bereits andere Module aktiv sind, kommasepariert: +local_modules = "BIBBNeo" +``` + +### 3. Cache leeren + +```bash +rm -rf /usr/local/vufind/local/cache/configs/* +rm -rf /usr/local/vufind/local/cache/objects/* +``` + +### 4. Apache neustarten + +```bash +systemctl restart apache2 +``` + +## Voraussetzungen + +- Solr-Feld `recordtype` muss für DSpace-Records den Wert `dspace` enthalten +- Alle BIBB-spezifischen Solr-Felder müssen im Schema definiert sein + +## Noch zu migrieren + +Die folgenden Dateien aus der alten Installation müssen noch +ins BIBBNeo-Modul überführt werden: + +- [ ] Citation.php (BIBB/BWP-Zitierformat) +- [ ] KohaILSDI.php (Koha-Treiber-Anpassungen) +- [ ] RecordDataFormatterFactory.php (Detailanzeige) +- [ ] Export.php (RIS-Export-Anpassungen) +- [ ] BrowseController.php +- [ ] CartController.php +- [ ] CoverController.php +- [ ] FeedbackController.php +- [ ] AjaxController.php +- [ ] Theme BIBBNeo (Templates, CSS, JS) diff --git a/config/module.config.php b/config/module.config.php new file mode 100644 index 0000000..8e370d9 --- /dev/null +++ b/config/module.config.php @@ -0,0 +1,51 @@ + [ + 'allow_override' => true, + ], + + 'vufind' => [ + 'plugin_managers' => [ + // Register custom RecordDrivers + 'recorddriver' => [ + 'factories' => [ + // DSpace RecordDriver + \BIBBNeo\RecordDriver\SolrDspace::class => + \BIBBNeo\RecordDriver\SolrDspaceFactory::class, + // Extended BIBB MARC RecordDriver + \BIBBNeo\RecordDriver\SolrMarc::class => + \BIBBNeo\RecordDriver\SolrMarcFactory::class, + ], + 'aliases' => [ + // VuFind uses recordtype field to resolve driver: + // recordtype "dspace" → SolrDspace driver + 'SolrDspace' => \BIBBNeo\RecordDriver\SolrDspace::class, + // Override default SolrMarc with BIBB version + \VuFind\RecordDriver\SolrMarc::class => + \BIBBNeo\RecordDriver\SolrMarc::class, + ], + ], + ], + + // Disable SimilarItemsCarousel (previously done via module.config.php hack) + // Note: In VuFind 10.2 this can also be configured via config.ini + // [Record] -> tabs setting. Keeping it here for explicitness. + ], +]; + +return $config; diff --git a/src/BIBBNeo/Module.php b/src/BIBBNeo/Module.php new file mode 100644 index 0000000..f5a37e8 --- /dev/null +++ b/src/BIBBNeo/Module.php @@ -0,0 +1,48 @@ + [ + 'namespaces' => [ + __NAMESPACE__ => __DIR__, + ], + ], + ]; + } +} diff --git a/src/BIBBNeo/RecordDriver/BIBBRecordTrait.php b/src/BIBBNeo/RecordDriver/BIBBRecordTrait.php new file mode 100644 index 0000000..c5b82b2 --- /dev/null +++ b/src/BIBBNeo/RecordDriver/BIBBRecordTrait.php @@ -0,0 +1,400 @@ +fields['citation']) + && is_array($this->fields['citation']) + ? $this->fields['citation'] : []; + } + + /** + * Get the citationdr of the current record. + * + * @return array + */ + public function getCitationsDrs() + { + return isset($this->fields['citationdr']) + && is_array($this->fields['citationdr']) + ? $this->fields['citationdr'] : []; + } + + /** + * Get the BIBB extend of the current record. + * + * @return array + */ + public function getExtend() + { + return isset($this->fields['extend']) + && is_array($this->fields['extend']) + ? $this->fields['extend'] : []; + } + + /** + * Get the BIBB participation of the current record. + * + * @return array + */ + public function getParticipations() + { + return isset($this->fields['participation']) + && is_array($this->fields['participation']) + ? $this->fields['participation'] : []; + } + + /** + * Get the BIBB research focus of the current record. + * + * @return array + */ + public function getResearchFocus() + { + return isset($this->fields['researchfocus']) + && is_array($this->fields['researchfocus']) + ? $this->fields['researchfocus'] : []; + } + + /** + * Get the accompanying material of the current record. + * + * @return array + */ + public function getAccompanyingMaterial() + { + return isset($this->fields['accompanyingmaterial']) + && is_array($this->fields['accompanyingmaterial']) + ? $this->fields['accompanyingmaterial'] : []; + } + + /** + * Get the additional links of the current record. + * + * @return array + */ + public function getAdditionalLinks() + { + return isset($this->fields['additionallinks']) + && is_array($this->fields['additionallinks']) + ? $this->fields['additionallinks'] : []; + } + + /** + * Get the dissertation note of the current record. + * + * @return array + */ + public function getDissertationnote() + { + return isset($this->fields['dissertationnote']) + && is_array($this->fields['dissertationnote']) + ? $this->fields['dissertationnote'] : []; + } + + /** + * Get the original version of the current record. + * + * @return array + */ + public function getOriginalVersion() + { + return isset($this->fields['originalversion']) + && is_array($this->fields['originalversion']) + ? $this->fields['originalversion'] : []; + } + + /** + * Get the review-of reference for the current record. + * + * @return array + */ + public function getReviewOf() + { + return isset($this->fields['reviewof']) + ? $this->fields['reviewof'] : []; + } + + /** + * Get the replaces field. + * + * @return string + */ + public function getReplaces() + { + return isset($this->fields['replaces']) + ? $this->fields['replaces'] : ''; + } + + /** + * Get the isReplacedBy field. + * + * @return string + */ + public function getIsReplacedBy() + { + return isset($this->fields['isreplacedby']) + ? $this->fields['isreplacedby'] : ''; + } + + /** + * Get an array of all Classification entries. + * + * @return array + */ + public function getClassifications() + { + return isset($this->fields['classification']) + && is_array($this->fields['classification']) + ? $this->fields['classification'] : []; + } + + /** + * Get inventories for the record. + * + * @return array + */ + public function getInventories() + { + return []; + } + + /** + * Get inventory gaps for the record. + * + * @return array + */ + public function getInventoryGaps() + { + return []; + } + + /** + * Get the control number. + * + * @return mixed + */ + public function getCtrlNum() + { + return !empty($this->fields['ctrlnum']) + ? $this->fields['ctrlnum'][0] + : []; + } + + /** + * Get the VOE-VZ link(s). + * + * @return array + */ + public function getVoeVzLink() + { + return isset($this->fields['voevzlink']) + ? $this->fields['voevzlink'] : []; + } + + // --------------------------------------------------------------- + // Overridden core methods with BIBB-specific behavior + // --------------------------------------------------------------- + + /** + * Get all subject headings - BIBB uses only 'topic', not + * geographic, genre, or era. + * + * @param bool $extended Whether to return a keyed array + * + * @return array + */ + public function getAllSubjectHeadings($extended = false) + { + $headings = []; + // BIBB: only use 'topic' field + foreach (['topic'] as $field) { + if (isset($this->fields[$field])) { + $headings = array_merge($headings, $this->fields[$field]); + } + } + + $callback = function ($i) use ($extended) { + return $extended + ? ['heading' => [$i], 'type' => '', 'source' => ''] + : [$i]; + }; + return array_map($callback, array_unique($headings)); + } + + /** + * Get formats - BIBB prioritizes 'medium' field over 'format'. + * + * @return array + */ + public function getFormats() + { + if (isset($this->fields['medium'])) { + return $this->fields['medium']; + } + return isset($this->fields['format']) + ? $this->fields['format'] : []; + } + + /** + * Get general notes - BIBB uses 'additionalremarks' field. + * + * @return array + */ + public function getGeneralNotes() + { + return isset($this->fields['additionalremarks']) + ? $this->fields['additionalremarks'] : []; + } + + /** + * Get series - BIBB combines series and series2. + * + * @return array + */ + public function getSeries() + { + $fields = []; + if (isset($this->fields['series']) && !empty($this->fields['series'])) { + $val = $this->fields['series']; + if (is_array($val)) { + foreach ($val as $s) { + $fields[] = ['name' => $s, 'number' => '']; + } + } else { + $fields[] = ['name' => $val, 'number' => '']; + } + } + if (isset($this->fields['series2']) && !empty($this->fields['series2'])) { + $val = $this->fields['series2']; + if (is_array($val)) { + foreach ($val as $s) { + $fields[] = ['name' => $s, 'number' => '']; + } + } else { + $fields[] = ['name' => $val, 'number' => '']; + } + } + return $fields; + } + + /** + * Get the first part of the title (before colon). + * + * @return string + */ + public function getMainTitle() + { + $t = ''; + if (isset($this->fields['title'])) { + if (strpos($this->fields['title'], ':') !== false) { + $t = trim(strstr($this->fields['title'], ':', true)); + } else { + $t = $this->fields['title']; + } + } + return $t; + } + + /** + * Get container volume - handles array values with "und" separator. + * + * @return string + */ + public function getContainerVolume() + { + $ret = ''; + if (isset($this->fields['container_volume'])) { + if (is_array($this->fields['container_volume'])) { + $ret = implode(' und ', $this->fields['container_volume']); + } else { + $ret = $this->fields['container_volume']; + } + } + return $ret; + } + + /** + * Get the review status / contents of the title. + * + * @return string + */ + public function getContents() + { + return isset($this->fields['contents']) + ? $this->fields['contents'] : ''; + } + + /** + * Get the alternative title of the record. + * + * @return string + */ + public function getAlternativeTitle() + { + return isset($this->fields['title_alt']) + ? $this->fields['title_alt'] : ''; + } + + /** + * Get the collection(s) for the record. + * + * @return array + */ + public function getCollection() + { + if (isset($this->fields['collection']) + && !empty($this->fields['collection']) + ) { + return is_array($this->fields['collection']) + ? $this->fields['collection'] + : [$this->fields['collection']]; + } + return []; + } + + /** + * Get supported citation formats - BIBB custom formats. + * + * @return array + */ + protected function getSupportedCitationFormats() + { + return ['BIBB', 'BIBBBWP', 'APA']; + } + + /** + * Get TOC - disabled for BIBB. + * + * @return array + */ + public function getTOC() + { + return []; + } +} diff --git a/src/BIBBNeo/RecordDriver/SolrDspace.php b/src/BIBBNeo/RecordDriver/SolrDspace.php new file mode 100644 index 0000000..6074a8a --- /dev/null +++ b/src/BIBBNeo/RecordDriver/SolrDspace.php @@ -0,0 +1,439 @@ +fields['filetype']) + && is_array($this->fields['filetype']) + ? $this->fields['filetype'] : []; + } + + /** + * Get the license of the current record. + * + * @return array + */ + public function getLicense() + { + return isset($this->fields['license']) + && is_array($this->fields['license']) + ? $this->fields['license'] : []; + } + + /** + * Get the rights license of the current record. + * + * @return array + */ + public function getRightsLicense() + { + return isset($this->fields['rightslicense']) + && is_array($this->fields['rightslicense']) + ? $this->fields['rightslicense'] : []; + } + + /** + * Get the rights URI of the current record. + * + * @return array + */ + public function getRightsUri() + { + return isset($this->fields['rightsuri']) + && is_array($this->fields['rightsuri']) + ? $this->fields['rightsuri'] : []; + } + + /** + * Get the rights holder of the current record. + * + * @return array + */ + public function getRightsHolder() + { + return isset($this->fields['rightsholder']) + && is_array($this->fields['rightsholder']) + ? $this->fields['rightsholder'] : []; + } + + /** + * Get the embargo terms of the current record. + * + * @return array + */ + public function getEmbargoTerms() + { + return isset($this->fields['embargoterms']) + && is_array($this->fields['embargoterms']) + ? $this->fields['embargoterms'] : []; + } + + /** + * Get the embargo lift date of the current record. + * + * @return array + */ + public function getEmbargoLiftDate() + { + return isset($this->fields['embargoliftdate']) + && is_array($this->fields['embargoliftdate']) + ? $this->fields['embargoliftdate'] : []; + } + + // --------------------------------------------------------------- + // DSpace-specific URL handling + // --------------------------------------------------------------- + + /** + * Base URL for BIBB DSpace instance. + */ + protected const DSPACE_BASE_URL = 'https://bibb-dspace.bibb.de'; + + /** + * Check if a URL is a DSpace handle URL that should be filtered out. + * + * @param string $url URL to check + * + * @return bool + */ + protected function isDspaceHandleUrl($url) + { + $patterns = [ + self::DSPACE_BASE_URL . '/handle/BIBB', + self::DSPACE_BASE_URL . '/jspui/handle/BIBB', + ]; + foreach ($patterns as $pattern) { + if (strncmp($url, $pattern, strlen($pattern)) === 0) { + return true; + } + } + return false; + } + + /** + * Check if a URL is a DSpace bitstream REST URL. + * + * @param string $url URL to check + * + * @return bool + */ + protected function isDspaceBitstreamUrl($url) + { + return strncmp( + $url, + self::DSPACE_BASE_URL . '/rest/bitstreams', + strlen(self::DSPACE_BASE_URL . '/rest/bitstreams') + ) === 0; + } + + /** + * Parse a URL string to extract description and URL parts. + * + * Handles patterns like: + * "http://example.com (Description)" + * "http://example.com [Description]" + * + * @param string $pid Raw URL/PID string + * + * @return array with keys 'url' and 'desc' + */ + protected function parseUrlDescription($pid) + { + $url = $pid; + $desc = ''; + + // Check for description in parentheses + if (($start = strrpos($pid, '(')) !== false) { + $end = strrpos($pid, ')'); + if ($end !== false) { + $desc = substr($pid, $start + 1, $end - $start - 1); + $url = trim(substr($pid, 0, $start)); + $url = str_replace('(Volltext)', '', $url); + } + } + + // Check for description in brackets + if (($start = strrpos($pid, '[')) !== false) { + $end = strrpos($pid, ']'); + if ($end !== false) { + $desc = substr($pid, $start + 1, $end - $start - 1); + $url = trim(substr($pid, 0, $start)); + } + } + + // Fix bare www. URLs + if (strncmp($url, 'www.bibb.de', strlen('www.bibb.de')) === 0) { + $url = 'https://' . $url; + } + + if (strlen($url) === 0) { + $url = $pid; + } + + $url = trim(str_replace('(Volltext)', '', $url)); + + return ['url' => $url, 'desc' => $desc]; + } + + /** + * Resolve a URN to a URL. + * + * @param string $urn URN string + * + * @return array Link array with desc, displaytext, url + */ + protected function resolveUrn($urn) + { + return [ + 'desc' => 'Volltext über anderen Anbieter (ggf. kostenpflichtig)', + 'displaytext' => $urn, + 'url' => 'https://nbn-resolving.org/' . urlencode($urn), + ]; + } + + /** + * Resolve a DOI to a URL. + * + * @param string $pid Raw DOI string (with prefix) + * + * @return array Link array with desc, displaytext, url + */ + protected function resolveDoi($pid) + { + $lower = strtolower($pid); + $start = 0; + if (strncmp($lower, 'doi:', 4) === 0) { + $start = 4; + } elseif (strncmp($lower, 'http://dx.doi.org/', 18) === 0) { + $start = 18; + } + + return [ + 'desc' => 'Volltext über anderen Anbieter (ggf. kostenpflichtig)', + 'displaytext' => $pid, + 'url' => 'https://doi.org/' . urlencode(trim(substr($pid, $start, 100))), + ]; + } + + /** + * Check if a string is a DOI. + * + * @param string $pid URL/PID string + * + * @return bool + */ + protected function isDoi($pid) + { + $lower = strtolower($pid); + return strncmp($lower, 'doi:', 4) === 0 + || strncmp($lower, 'http://dx.doi.org/', 18) === 0; + } + + /** + * Check if a string is a URN. + * + * @param string $pid URL/PID string + * + * @return bool + */ + protected function isUrn($pid) + { + return strncmp($pid, 'urn:nbn:de:', 11) === 0; + } + + /** + * Get online links with full URL resolution and description parsing. + * + * This is the main method for building display links for DSpace records. + * It handles DSpace bitstreams, DOIs, URNs, VOE-VZ links, and + * generic URLs with label extraction. + * + * @param bool $expand If true, fall back to PID-based links when no + * direct links are found + * + * @return array Array of associative arrays with keys: + * desc, displaytext, url + */ + public function getOnlineLinks($expand = false) + { + if (!isset($this->fields['url'])) { + return []; + } + + $retPids = []; + $ret = []; + $voevzlink = $this->getVoeVzLink(); + $bitstreamIndex = 0; + + foreach ($this->fields['url'] as $pid) { + // Skip DSpace handle URLs (these are internal) + if ($this->isDspaceHandleUrl($pid)) { + continue; + } + + // Handle URNs + if ($this->isUrn($pid)) { + $retPids[] = $this->resolveUrn($pid); + continue; + } + + // Handle DOIs + if ($this->isDoi($pid)) { + $retPids[] = $this->resolveDoi($pid); + continue; + } + + // Handle DSpace bitstream REST URLs + $url = ''; + $desc = ''; + + if ($this->isDspaceBitstreamUrl($pid)) { + // Use VOE-VZ link if available, otherwise use bitstream URL + $url = isset($voevzlink[$bitstreamIndex]) + ? $voevzlink[$bitstreamIndex] + : $pid; + $bitstreamIndex++; + + $desc = 'Volltext'; + // Extract description from parentheses if present + if (($start = strrpos($pid, '(')) !== false) { + $end = strrpos($pid, ')'); + if ($end !== false) { + $desc = substr($pid, $start + 1, $end - $start - 1); + } + } + } + + // Parse description from URL string (parentheses/brackets) + $parsed = $this->parseUrlDescription($pid); + if (!empty($parsed['desc'])) { + $desc = $parsed['desc']; + } + if (empty($url)) { + $url = $parsed['url']; + } + + $ret[] = [ + 'desc' => $desc, + 'displaytext' => $pid, + 'url' => $url, + ]; + } + + if (!$expand) { + return $ret; + } + + // In expand mode: fall back to PID-based links if no direct links found + return count($ret) === 0 ? $retPids : $ret; + } + + /** + * Get persistent identifiers (DOIs, URNs) excluding DSpace handles. + * + * @return array Array of associative arrays with displaytext and link keys + */ + public function getPids() + { + if (!isset($this->fields['url'])) { + return []; + } + + $ret = []; + foreach ($this->fields['url'] as $pid) { + // Skip DSpace handle URLs + if ($this->isDspaceHandleUrl($pid)) { + continue; + } + + // Collect URNs + if ($this->isUrn($pid)) { + $ret[] = [ + 'displaytext' => $pid, + 'link' => 'https://nbn-resolving.org/' . urlencode($pid), + ]; + continue; + } + + // Collect DOIs + if ($this->isDoi($pid)) { + $resolved = $this->resolveDoi($pid); + $ret[] = [ + 'displaytext' => $pid, + 'link' => $resolved['url'], + ]; + } + } + + return $ret; + } + + /** + * Get formats, filtered to remove "Electronic resource" when only + * DSpace links are present (no external electronic links). + * + * @return array + */ + public function getFormatsFiltered() + { + $formats = $this->getFormats(); + $hasElectronic = in_array('Electronic resource', $formats); + + if (!$hasElectronic) { + return $formats; + } + + // Remove "Electronic resource" from format list + $filtered = array_filter($formats, function ($f) { + return $f !== 'Electronic resource'; + }); + + // Check if there are non-DSpace URLs that justify "Electronic resource" + $urls = isset($this->fields['url']) && is_array($this->fields['url']) + ? $this->fields['url'] : []; + + $hasExternalUrls = false; + foreach ($urls as $url) { + if (!$this->isDspaceHandleUrl($url) + && strncmp($url, self::DSPACE_BASE_URL, strlen(self::DSPACE_BASE_URL)) !== 0 + ) { + $hasExternalUrls = true; + break; + } + } + + // Only re-add "Electronic resource" if there are external URLs + if ($hasExternalUrls) { + $filtered[] = 'Electronic resource'; + } + + return array_values($filtered); + } +} diff --git a/src/BIBBNeo/RecordDriver/SolrDspaceFactory.php b/src/BIBBNeo/RecordDriver/SolrDspaceFactory.php new file mode 100644 index 0000000..304c66a --- /dev/null +++ b/src/BIBBNeo/RecordDriver/SolrDspaceFactory.php @@ -0,0 +1,40 @@ +getMarcReader() belong here, not in the trait. +} diff --git a/src/BIBBNeo/RecordDriver/SolrMarcFactory.php b/src/BIBBNeo/RecordDriver/SolrMarcFactory.php new file mode 100644 index 0000000..2569c1c --- /dev/null +++ b/src/BIBBNeo/RecordDriver/SolrMarcFactory.php @@ -0,0 +1,40 @@ +