|
-
+
+
|
-
+
+
|
{{ entry.creator }}
@@ -101,6 +103,7 @@ import PublicationDisplay from "@/components/PublicationDisplay.vue";
import SortArrow from "@/components/SortArrow.vue";
import {resolveiI18N} from "@/api/I18N.ts";
import Modal from "@/components/Modal.vue";
+import PersonDisplay from "@/components/PersonDisplay.vue";
const deleteModal = useTemplateRef("deleteModalRef");
diff --git a/ubo-common/src/test/java/org/mycore/ubo/dedup/TestDeDupCriteriaBuilder.java b/ubo-common/src/test/java/org/mycore/ubo/dedup/TestDeDupCriteriaBuilder.java
index 8aac62a40..5641c903f 100644
--- a/ubo-common/src/test/java/org/mycore/ubo/dedup/TestDeDupCriteriaBuilder.java
+++ b/ubo-common/src/test/java/org/mycore/ubo/dedup/TestDeDupCriteriaBuilder.java
@@ -9,15 +9,13 @@
package org.mycore.ubo.dedup;
+import org.jdom2.Element;
import org.junit.Test;
-import junit.framework.TestCase;
+import org.mycore.common.MCRConstants;
import org.mycore.common.MCRJPATestCase;
-import org.mycore.ubo.dedup.DeDupCriteriaBuilder;
-import org.mycore.ubo.dedup.DeDupCriterion;
-import static org.junit.Assert.assertEquals;
-import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.*;
public class TestDeDupCriteriaBuilder extends MCRJPATestCase {
@@ -30,7 +28,7 @@ public void testIdentifiers() {
assertEquals(c1, c2);
DeDupCriterion c3 = builder.buildFromIdentifier("doi", "10.1002/0470841559.ch1");
- assertFalse(c1.equals(c3));
+ assertNotEquals(c1, c3);
DeDupCriterion c4 = builder.buildFromIdentifier("isbn", "978-1-56619-909-4" );
DeDupCriterion c5 = builder.buildFromIdentifier("isbn", "9781566199094" );
@@ -42,7 +40,7 @@ public void testTitles() {
DeDupCriterion c0 = builder.buildFromTitleAuthor("A different short title","Meier");
DeDupCriterion c1 = builder.buildFromTitleAuthor("This is a short title","Meier");
DeDupCriterion c2 = builder.buildFromTitleAuthor("THIS is a SHORT Title","Meier");
- assertFalse(c0.equals(c1));
+ assertNotEquals(c0, c1);
assertEquals(c1, c2);
DeDupCriterion c3 = builder.buildFromTitleAuthor("Der Moiré-Effekt im Zusammenhang mit Hühneraugen","Meier");
@@ -51,10 +49,56 @@ public void testTitles() {
DeDupCriterion c5 = builder.buildFromTitleAuthor("Horizon-Report 2015","Meier");
DeDupCriterion c6 = builder.buildFromTitleAuthor("Horizon-Report 2016","Meier");
- assertFalse(c5.equals(c6));
+ assertNotEquals(c5, c6);
DeDupCriterion c7 = builder.buildFromTitleAuthor("Hier ist Augenmaß gefragt","Meier");
DeDupCriterion c8 = builder.buildFromTitleAuthor("Hier ist Augenmass gefragt","Meier");
assertEquals(c7, c8);
}
+
+ @Test
+ public void testNames() {
+ Element firstName1 = createElement("Peter", "given", "namePart");
+ Element firstName2 = createElement("Hans", "given", "namePart");
+ Element firstName3 = createElement("Hans P.", "given", "namePart");
+ Element familyName1 = createElement("Müller", "family", "namePart");
+ Element familyName2 = createElement("Mueller", "family", "namePart");
+ Element familyName3 = createElement("Mueller-Meyer", "family", "namePart");
+
+ DeDupCriterion c1 = builder.buildFromFullName(firstName1, familyName1);
+ DeDupCriterion c2 = builder.buildFromFullName(firstName1, familyName2);
+ DeDupCriterion c3 = builder.buildFromFullName(firstName1, familyName3);
+ DeDupCriterion c4 = builder.buildFromFullName(firstName2, familyName1);
+ DeDupCriterion c5 = builder.buildFromFullName(firstName3, familyName1);
+
+ assertEquals(c1, c2);
+ assertNotEquals(c2, c3);
+ assertNotEquals(c1, c4);
+ assertNotEquals(c4, c5);
+ }
+
+ @Test
+ public void testNameIdentifiers() {
+ Element identifier1 = createElement("12345", "lsf", "nameIdentifier");
+ Element identifier2 = createElement("12345", "lsf", "nameIdentifier");
+ Element identifier3 = createElement("54321", "lsf", "nameIdentifier");
+ Element identifier4 = createElement("12345", "scopus", "nameIdentifier");
+
+ DeDupCriterion c1 = builder.buildFromNameIdentifier(identifier1);
+ DeDupCriterion c2 = builder.buildFromNameIdentifier(identifier2);
+ DeDupCriterion c3 = builder.buildFromNameIdentifier(identifier3);
+ DeDupCriterion c4 = builder.buildFromNameIdentifier(identifier4);
+
+ assertEquals(c1, c2);
+ assertNotEquals(c1, c3);
+ assertNotEquals(c1, c4);
+ assertNotEquals(c3, c4);
+ }
+
+ public static Element createElement(String value, String type, String elementName) {
+ Element element = new Element(elementName, MCRConstants.MODS_NAMESPACE);
+ element.setAttribute("type", type);
+ element.setText(value);
+ return element;
+ }
}
diff --git a/ubo-common/src/test/java/org/mycore/ubo/dedup/jpa/DeduplicationCriterionResolverTest.java b/ubo-common/src/test/java/org/mycore/ubo/dedup/jpa/DeduplicationCriterionResolverTest.java
new file mode 100644
index 000000000..57248d96d
--- /dev/null
+++ b/ubo-common/src/test/java/org/mycore/ubo/dedup/jpa/DeduplicationCriterionResolverTest.java
@@ -0,0 +1,71 @@
+/**
+ * Copyright (c) 2016 Duisburg-Essen University Library
+ *
+ * All rights reserved. This program and the accompanying materials
+ * are made available under the terms of the GNU Public License v2.0
+ * which accompanies this distribution, and is available at
+ * http://www.gnu.org/licenses/old-licenses/gpl-2.0.html
+ **/
+
+package org.mycore.ubo.dedup.jpa;
+
+import org.jdom2.Document;
+import org.jdom2.Element;
+import org.jdom2.input.SAXBuilder;
+import org.jdom2.transform.JDOMSource;
+import org.junit.Test;
+import org.mycore.common.MCRStoreTestCase;
+import org.mycore.common.config.MCRConfiguration2;
+import org.mycore.common.content.MCRURLContent;
+import org.mycore.common.xml.MCRURIResolver;
+import org.mycore.datamodel.metadata.MCRMetadataManager;
+import org.mycore.datamodel.metadata.MCRObject;
+import org.mycore.datamodel.metadata.MCRObjectMetadataTest;
+
+import java.net.URL;
+import java.util.Comparator;
+import java.util.List;
+
+import static org.junit.Assert.assertEquals;
+
+public class DeduplicationCriterionResolverTest extends MCRStoreTestCase {
+
+ public static final String DEDUP_CALL = "dedup:search:person:junit_modsperson_00000001";
+
+ @Override
+ public void setUp() throws Exception {
+ super.setUp();
+ MCRConfiguration2.set("MCR.URIResolver.ModuleResolver.dedup", "org.mycore.ubo.dedup.jpa.DeduplicationCriterionResolver");
+
+ URL url1 = MCRObjectMetadataTest.class.getResource("/DeduplicationKeyManagerTest/junit_modsperson_00000001.xml");
+ Document doc1 = new MCRURLContent(url1).asXML();
+ MCRObject obj1 = new MCRObject(doc1);
+
+ URL url2 = MCRObjectMetadataTest.class.getResource("/DeduplicationKeyManagerTest/junit_modsperson_00000002.xml");
+ Document doc2 = new MCRURLContent(url2).asXML();
+ MCRObject obj2 = new MCRObject(doc2);
+
+ URL url3 = MCRObjectMetadataTest.class.getResource("/DeduplicationKeyManagerTest/junit_modsperson_00000003.xml");
+ Document doc3 = new MCRURLContent(url3).asXML();
+ MCRObject obj3 = new MCRObject(doc3);
+
+ MCRMetadataManager.create(obj1);
+ MCRMetadataManager.create(obj2);
+ MCRMetadataManager.create(obj3);
+ }
+
+ @Test
+ public void testDeduplicationCriterionResolverPerson() throws Exception {
+ JDOMSource source = (JDOMSource) MCRURIResolver.instance().resolve(DEDUP_CALL, null);
+ SAXBuilder builder = new SAXBuilder();
+ Document document = builder.build(source.getInputSource());
+ List listSource = document.getRootElement().getChildren();
+
+ assertEquals(2, listSource.size());
+ listSource.sort(Comparator.comparing(e -> e.getAttributeValue("id")));
+ Element duplicate1 = listSource.get(0);
+ assertEquals("junit_modsperson_00000002", duplicate1.getAttributeValue("id"));
+ Element duplicate2 = listSource.get(1);
+ assertEquals("junit_modsperson_00000003", duplicate2.getAttributeValue("id"));
+ }
+}
diff --git a/ubo-common/src/test/java/org/mycore/ubo/dedup/jpa/DeduplicationKeyManagerTest.java b/ubo-common/src/test/java/org/mycore/ubo/dedup/jpa/DeduplicationKeyManagerTest.java
new file mode 100644
index 000000000..97975fda8
--- /dev/null
+++ b/ubo-common/src/test/java/org/mycore/ubo/dedup/jpa/DeduplicationKeyManagerTest.java
@@ -0,0 +1,86 @@
+/**
+ * Copyright (c) 2016 Duisburg-Essen University Library
+ *
+ * All rights reserved. This program and the accompanying materials
+ * are made available under the terms of the GNU Public License v2.0
+ * which accompanies this distribution, and is available at
+ * http://www.gnu.org/licenses/old-licenses/gpl-2.0.html
+ **/
+
+package org.mycore.ubo.dedup.jpa;
+
+import org.jdom2.Document;
+import org.jdom2.Element;
+import org.jdom2.JDOMException;
+import org.junit.Before;
+import org.junit.Test;
+import org.mycore.access.MCRAccessException;
+import org.mycore.common.MCRStoreTestCase;
+import org.mycore.common.content.MCRURLContent;
+import org.mycore.datamodel.metadata.MCRMetadataManager;
+import org.mycore.datamodel.metadata.MCRObject;
+import org.mycore.datamodel.metadata.MCRObjectMetadataTest;
+import org.mycore.ubo.dedup.DeDupCriteriaBuilder;
+import org.mycore.ubo.dedup.DeDupCriterion;
+import org.mycore.ubo.dedup.PossibleDuplicate;
+
+import java.io.IOException;
+import java.net.URL;
+import java.util.List;
+
+import static org.junit.Assert.assertEquals;
+import static org.mycore.ubo.dedup.TestDeDupCriteriaBuilder.createElement;
+import static org.mycore.ubo.dedup.jpa.DeduplicationKeyManager.PERSON_CATEGORY_GROUP;
+import static org.mycore.ubo.dedup.jpa.DeduplicationKeyManager.SortOrder.*;
+
+public class DeduplicationKeyManagerTest extends MCRStoreTestCase {
+
+ private DeduplicationKeyManager deduplicationKeyManager;
+ private DeDupCriteriaBuilder builder;
+
+ @Before
+ public void before() throws IOException, JDOMException, MCRAccessException {
+ deduplicationKeyManager = new DeduplicationKeyManager();
+ builder = new DeDupCriteriaBuilder();
+ URL url1 = MCRObjectMetadataTest.class.getResource("/DeduplicationKeyManagerTest/junit_modsperson_00000001.xml");
+ Document doc1 = new MCRURLContent(url1).asXML();
+ MCRObject obj1 = new MCRObject(doc1);
+
+ URL url2 = MCRObjectMetadataTest.class.getResource("/DeduplicationKeyManagerTest/junit_modsperson_00000002.xml");
+ Document doc2 = new MCRURLContent(url2).asXML();
+ MCRObject obj2 = new MCRObject(doc2);
+
+ MCRMetadataManager.create(obj1);
+ MCRMetadataManager.create(obj2);
+ }
+
+ @Test
+ public void testGetDuplicatesPerson() {
+ List dedupKeys = deduplicationKeyManager.getDuplicates(NONE, ASC, PERSON_CATEGORY_GROUP);
+ assertEquals(2, dedupKeys.size());
+ PossibleDuplicate dup1 = dedupKeys.get(0);
+ PossibleDuplicate dup2 = dedupKeys.get(1);
+ assertEquals("junit_modsperson_00000001", dup1.getMcrId1());
+ assertEquals("junit_modsperson_00000002", dup1.getMcrId2());
+ assertEquals("junit_modsperson_00000001", dup2.getMcrId1());
+ assertEquals("junit_modsperson_00000002", dup2.getMcrId2());
+
+ Element firstName = createElement("Lisa", "given", "namePart");
+ Element familyName = createElement("Müller", "given", "namePart");
+ DeDupCriterion nameCriterion = builder.buildFromFullName(firstName, familyName);
+
+ Element identifier1 = createElement("11111", "lsf", "nameIdentifier");
+ DeDupCriterion identifierCriterion = builder.buildFromNameIdentifier(identifier1);
+
+ assertEquals(nameCriterion.getKey(), dup1.getDeduplicationKey());
+ assertEquals(identifierCriterion.getKey(), dup2.getDeduplicationKey());
+
+ dedupKeys = deduplicationKeyManager.getDuplicates(NONE, DESC, PERSON_CATEGORY_GROUP);
+ assertEquals(2, dedupKeys.size());
+ dup1 = dedupKeys.get(0);
+ dup2 = dedupKeys.get(1);
+
+ assertEquals(nameCriterion.getKey(), dup2.getDeduplicationKey());
+ assertEquals(identifierCriterion.getKey(), dup1.getDeduplicationKey());
+ }
+}
diff --git a/ubo-common/src/test/resources/DeduplicationKeyManagerTest/junit_modsperson_00000001.xml b/ubo-common/src/test/resources/DeduplicationKeyManagerTest/junit_modsperson_00000001.xml
new file mode 100644
index 000000000..583cb08df
--- /dev/null
+++ b/ubo-common/src/test/resources/DeduplicationKeyManagerTest/junit_modsperson_00000001.xml
@@ -0,0 +1,17 @@
+
+
+
+
+
+
+
+ Müller
+ Lisa
+ 11111
+ 1112222333
+
+
+
+
+
+
diff --git a/ubo-common/src/test/resources/DeduplicationKeyManagerTest/junit_modsperson_00000002.xml b/ubo-common/src/test/resources/DeduplicationKeyManagerTest/junit_modsperson_00000002.xml
new file mode 100644
index 000000000..eb761f64e
--- /dev/null
+++ b/ubo-common/src/test/resources/DeduplicationKeyManagerTest/junit_modsperson_00000002.xml
@@ -0,0 +1,17 @@
+
+
+
+
+
+
+
+ Müller
+ Lisa
+ 11111
+ 1112222444
+
+
+
+
+
+
diff --git a/ubo-common/src/test/resources/DeduplicationKeyManagerTest/junit_modsperson_00000003.xml b/ubo-common/src/test/resources/DeduplicationKeyManagerTest/junit_modsperson_00000003.xml
new file mode 100644
index 000000000..2fac17a6e
--- /dev/null
+++ b/ubo-common/src/test/resources/DeduplicationKeyManagerTest/junit_modsperson_00000003.xml
@@ -0,0 +1,17 @@
+
+
+
+
+
+
+
+ Müller
+ Lena
+ 22222
+ 1112222333
+
+
+
+
+
+
diff --git a/ubo-common/src/test/resources/mycore.properties b/ubo-common/src/test/resources/mycore.properties
index 9e11ea1ce..0de65d83d 100644
--- a/ubo-common/src/test/resources/mycore.properties
+++ b/ubo-common/src/test/resources/mycore.properties
@@ -4,3 +4,5 @@ MCR.user2.matching.lead_id=lsf
MCR.user2.matching.publication.connection.strategy=uuid
MCR.EventHandler.MCRObject.019.Class=org.mycore.ubo.publication.PublicationEventHandler
MCR.Access.Class=org.mycore.access.MCRAccessBaseImpl
+
+MCR.Metadata.Type.modsperson=true
diff --git a/ubo-modsperson/.gitignore b/ubo-modsperson/.gitignore
new file mode 100644
index 000000000..fbdf7ebb0
--- /dev/null
+++ b/ubo-modsperson/.gitignore
@@ -0,0 +1,3 @@
+/.classpath
+/.project
+/.settings/
diff --git a/ubo-modsperson/LICENSE.txt b/ubo-modsperson/LICENSE.txt
new file mode 100644
index 000000000..94a9ed024
--- /dev/null
+++ b/ubo-modsperson/LICENSE.txt
@@ -0,0 +1,674 @@
+ GNU GENERAL PUBLIC LICENSE
+ Version 3, 29 June 2007
+
+ Copyright (C) 2007 Free Software Foundation, Inc.
+ Everyone is permitted to copy and distribute verbatim copies
+ of this license document, but changing it is not allowed.
+
+ Preamble
+
+ The GNU General Public License is a free, copyleft license for
+software and other kinds of works.
+
+ The licenses for most software and other practical works are designed
+to take away your freedom to share and change the works. By contrast,
+the GNU General Public License is intended to guarantee your freedom to
+share and change all versions of a program--to make sure it remains free
+software for all its users. We, the Free Software Foundation, use the
+GNU General Public License for most of our software; it applies also to
+any other work released this way by its authors. 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
+them 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 prevent others from denying you
+these rights or asking you to surrender the rights. Therefore, you have
+certain responsibilities if you distribute copies of the software, or if
+you modify it: responsibilities to respect the freedom of others.
+
+ For example, if you distribute copies of such a program, whether
+gratis or for a fee, you must pass on to the recipients the same
+freedoms that you received. 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.
+
+ Developers that use the GNU GPL protect your rights with two steps:
+(1) assert copyright on the software, and (2) offer you this License
+giving you legal permission to copy, distribute and/or modify it.
+
+ For the developers' and authors' protection, the GPL clearly explains
+that there is no warranty for this free software. For both users' and
+authors' sake, the GPL requires that modified versions be marked as
+changed, so that their problems will not be attributed erroneously to
+authors of previous versions.
+
+ Some devices are designed to deny users access to install or run
+modified versions of the software inside them, although the manufacturer
+can do so. This is fundamentally incompatible with the aim of
+protecting users' freedom to change the software. The systematic
+pattern of such abuse occurs in the area of products for individuals to
+use, which is precisely where it is most unacceptable. Therefore, we
+have designed this version of the GPL to prohibit the practice for those
+products. If such problems arise substantially in other domains, we
+stand ready to extend this provision to those domains in future versions
+of the GPL, as needed to protect the freedom of users.
+
+ Finally, every program is threatened constantly by software patents.
+States should not allow patents to restrict development and use of
+software on general-purpose computers, but in those that do, we wish to
+avoid the special danger that patents applied to a free program could
+make it effectively proprietary. To prevent this, the GPL assures that
+patents cannot be used to render the program non-free.
+
+ The precise terms and conditions for copying, distribution and
+modification follow.
+
+ TERMS AND CONDITIONS
+
+ 0. Definitions.
+
+ "This License" refers to version 3 of the GNU General Public License.
+
+ "Copyright" also means copyright-like laws that apply to other kinds of
+works, such as semiconductor masks.
+
+ "The Program" refers to any copyrightable work licensed under this
+License. Each licensee is addressed as "you". "Licensees" and
+"recipients" may be individuals or organizations.
+
+ To "modify" a work means to copy from or adapt all or part of the work
+in a fashion requiring copyright permission, other than the making of an
+exact copy. The resulting work is called a "modified version" of the
+earlier work or a work "based on" the earlier work.
+
+ A "covered work" means either the unmodified Program or a work based
+on the Program.
+
+ To "propagate" a work means to do anything with it that, without
+permission, would make you directly or secondarily liable for
+infringement under applicable copyright law, except executing it on a
+computer or modifying a private copy. Propagation includes copying,
+distribution (with or without modification), making available to the
+public, and in some countries other activities as well.
+
+ To "convey" a work means any kind of propagation that enables other
+parties to make or receive copies. Mere interaction with a user through
+a computer network, with no transfer of a copy, is not conveying.
+
+ An interactive user interface displays "Appropriate Legal Notices"
+to the extent that it includes a convenient and prominently visible
+feature that (1) displays an appropriate copyright notice, and (2)
+tells the user that there is no warranty for the work (except to the
+extent that warranties are provided), that licensees may convey the
+work under this License, and how to view a copy of this License. If
+the interface presents a list of user commands or options, such as a
+menu, a prominent item in the list meets this criterion.
+
+ 1. Source Code.
+
+ The "source code" for a work means the preferred form of the work
+for making modifications to it. "Object code" means any non-source
+form of a work.
+
+ A "Standard Interface" means an interface that either is an official
+standard defined by a recognized standards body, or, in the case of
+interfaces specified for a particular programming language, one that
+is widely used among developers working in that language.
+
+ The "System Libraries" of an executable work include anything, other
+than the work as a whole, that (a) is included in the normal form of
+packaging a Major Component, but which is not part of that Major
+Component, and (b) serves only to enable use of the work with that
+Major Component, or to implement a Standard Interface for which an
+implementation is available to the public in source code form. A
+"Major Component", in this context, means a major essential component
+(kernel, window system, and so on) of the specific operating system
+(if any) on which the executable work runs, or a compiler used to
+produce the work, or an object code interpreter used to run it.
+
+ The "Corresponding Source" for a work in object code form means all
+the source code needed to generate, install, and (for an executable
+work) run the object code and to modify the work, including scripts to
+control those activities. However, it does not include the work's
+System Libraries, or general-purpose tools or generally available free
+programs which are used unmodified in performing those activities but
+which are not part of the work. For example, Corresponding Source
+includes interface definition files associated with source files for
+the work, and the source code for shared libraries and dynamically
+linked subprograms that the work is specifically designed to require,
+such as by intimate data communication or control flow between those
+subprograms and other parts of the work.
+
+ The Corresponding Source need not include anything that users
+can regenerate automatically from other parts of the Corresponding
+Source.
+
+ The Corresponding Source for a work in source code form is that
+same work.
+
+ 2. Basic Permissions.
+
+ All rights granted under this License are granted for the term of
+copyright on the Program, and are irrevocable provided the stated
+conditions are met. This License explicitly affirms your unlimited
+permission to run the unmodified Program. The output from running a
+covered work is covered by this License only if the output, given its
+content, constitutes a covered work. This License acknowledges your
+rights of fair use or other equivalent, as provided by copyright law.
+
+ You may make, run and propagate covered works that you do not
+convey, without conditions so long as your license otherwise remains
+in force. You may convey covered works to others for the sole purpose
+of having them make modifications exclusively for you, or provide you
+with facilities for running those works, provided that you comply with
+the terms of this License in conveying all material for which you do
+not control copyright. Those thus making or running the covered works
+for you must do so exclusively on your behalf, under your direction
+and control, on terms that prohibit them from making any copies of
+your copyrighted material outside their relationship with you.
+
+ Conveying under any other circumstances is permitted solely under
+the conditions stated below. Sublicensing is not allowed; section 10
+makes it unnecessary.
+
+ 3. Protecting Users' Legal Rights From Anti-Circumvention Law.
+
+ No covered work shall be deemed part of an effective technological
+measure under any applicable law fulfilling obligations under article
+11 of the WIPO copyright treaty adopted on 20 December 1996, or
+similar laws prohibiting or restricting circumvention of such
+measures.
+
+ When you convey a covered work, you waive any legal power to forbid
+circumvention of technological measures to the extent such circumvention
+is effected by exercising rights under this License with respect to
+the covered work, and you disclaim any intention to limit operation or
+modification of the work as a means of enforcing, against the work's
+users, your or third parties' legal rights to forbid circumvention of
+technological measures.
+
+ 4. Conveying Verbatim Copies.
+
+ You may convey 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;
+keep intact all notices stating that this License and any
+non-permissive terms added in accord with section 7 apply to the code;
+keep intact all notices of the absence of any warranty; and give all
+recipients a copy of this License along with the Program.
+
+ You may charge any price or no price for each copy that you convey,
+and you may offer support or warranty protection for a fee.
+
+ 5. Conveying Modified Source Versions.
+
+ You may convey a work based on the Program, or the modifications to
+produce it from the Program, in the form of source code under the
+terms of section 4, provided that you also meet all of these conditions:
+
+ a) The work must carry prominent notices stating that you modified
+ it, and giving a relevant date.
+
+ b) The work must carry prominent notices stating that it is
+ released under this License and any conditions added under section
+ 7. This requirement modifies the requirement in section 4 to
+ "keep intact all notices".
+
+ c) You must license the entire work, as a whole, under this
+ License to anyone who comes into possession of a copy. This
+ License will therefore apply, along with any applicable section 7
+ additional terms, to the whole of the work, and all its parts,
+ regardless of how they are packaged. This License gives no
+ permission to license the work in any other way, but it does not
+ invalidate such permission if you have separately received it.
+
+ d) If the work has interactive user interfaces, each must display
+ Appropriate Legal Notices; however, if the Program has interactive
+ interfaces that do not display Appropriate Legal Notices, your
+ work need not make them do so.
+
+ A compilation of a covered work with other separate and independent
+works, which are not by their nature extensions of the covered work,
+and which are not combined with it such as to form a larger program,
+in or on a volume of a storage or distribution medium, is called an
+"aggregate" if the compilation and its resulting copyright are not
+used to limit the access or legal rights of the compilation's users
+beyond what the individual works permit. Inclusion of a covered work
+in an aggregate does not cause this License to apply to the other
+parts of the aggregate.
+
+ 6. Conveying Non-Source Forms.
+
+ You may convey a covered work in object code form under the terms
+of sections 4 and 5, provided that you also convey the
+machine-readable Corresponding Source under the terms of this License,
+in one of these ways:
+
+ a) Convey the object code in, or embodied in, a physical product
+ (including a physical distribution medium), accompanied by the
+ Corresponding Source fixed on a durable physical medium
+ customarily used for software interchange.
+
+ b) Convey the object code in, or embodied in, a physical product
+ (including a physical distribution medium), accompanied by a
+ written offer, valid for at least three years and valid for as
+ long as you offer spare parts or customer support for that product
+ model, to give anyone who possesses the object code either (1) a
+ copy of the Corresponding Source for all the software in the
+ product that is covered by this License, on a durable physical
+ medium customarily used for software interchange, for a price no
+ more than your reasonable cost of physically performing this
+ conveying of source, or (2) access to copy the
+ Corresponding Source from a network server at no charge.
+
+ c) Convey individual copies of the object code with a copy of the
+ written offer to provide the Corresponding Source. This
+ alternative is allowed only occasionally and noncommercially, and
+ only if you received the object code with such an offer, in accord
+ with subsection 6b.
+
+ d) Convey the object code by offering access from a designated
+ place (gratis or for a charge), and offer equivalent access to the
+ Corresponding Source in the same way through the same place at no
+ further charge. You need not require recipients to copy the
+ Corresponding Source along with the object code. If the place to
+ copy the object code is a network server, the Corresponding Source
+ may be on a different server (operated by you or a third party)
+ that supports equivalent copying facilities, provided you maintain
+ clear directions next to the object code saying where to find the
+ Corresponding Source. Regardless of what server hosts the
+ Corresponding Source, you remain obligated to ensure that it is
+ available for as long as needed to satisfy these requirements.
+
+ e) Convey the object code using peer-to-peer transmission, provided
+ you inform other peers where the object code and Corresponding
+ Source of the work are being offered to the general public at no
+ charge under subsection 6d.
+
+ A separable portion of the object code, whose source code is excluded
+from the Corresponding Source as a System Library, need not be
+included in conveying the object code work.
+
+ A "User Product" is either (1) a "consumer product", which means any
+tangible personal property which is normally used for personal, family,
+or household purposes, or (2) anything designed or sold for incorporation
+into a dwelling. In determining whether a product is a consumer product,
+doubtful cases shall be resolved in favor of coverage. For a particular
+product received by a particular user, "normally used" refers to a
+typical or common use of that class of product, regardless of the status
+of the particular user or of the way in which the particular user
+actually uses, or expects or is expected to use, the product. A product
+is a consumer product regardless of whether the product has substantial
+commercial, industrial or non-consumer uses, unless such uses represent
+the only significant mode of use of the product.
+
+ "Installation Information" for a User Product means any methods,
+procedures, authorization keys, or other information required to install
+and execute modified versions of a covered work in that User Product from
+a modified version of its Corresponding Source. The information must
+suffice to ensure that the continued functioning of the modified object
+code is in no case prevented or interfered with solely because
+modification has been made.
+
+ If you convey an object code work under this section in, or with, or
+specifically for use in, a User Product, and the conveying occurs as
+part of a transaction in which the right of possession and use of the
+User Product is transferred to the recipient in perpetuity or for a
+fixed term (regardless of how the transaction is characterized), the
+Corresponding Source conveyed under this section must be accompanied
+by the Installation Information. But this requirement does not apply
+if neither you nor any third party retains the ability to install
+modified object code on the User Product (for example, the work has
+been installed in ROM).
+
+ The requirement to provide Installation Information does not include a
+requirement to continue to provide support service, warranty, or updates
+for a work that has been modified or installed by the recipient, or for
+the User Product in which it has been modified or installed. Access to a
+network may be denied when the modification itself materially and
+adversely affects the operation of the network or violates the rules and
+protocols for communication across the network.
+
+ Corresponding Source conveyed, and Installation Information provided,
+in accord with this section must be in a format that is publicly
+documented (and with an implementation available to the public in
+source code form), and must require no special password or key for
+unpacking, reading or copying.
+
+ 7. Additional Terms.
+
+ "Additional permissions" are terms that supplement the terms of this
+License by making exceptions from one or more of its conditions.
+Additional permissions that are applicable to the entire Program shall
+be treated as though they were included in this License, to the extent
+that they are valid under applicable law. If additional permissions
+apply only to part of the Program, that part may be used separately
+under those permissions, but the entire Program remains governed by
+this License without regard to the additional permissions.
+
+ When you convey a copy of a covered work, you may at your option
+remove any additional permissions from that copy, or from any part of
+it. (Additional permissions may be written to require their own
+removal in certain cases when you modify the work.) You may place
+additional permissions on material, added by you to a covered work,
+for which you have or can give appropriate copyright permission.
+
+ Notwithstanding any other provision of this License, for material you
+add to a covered work, you may (if authorized by the copyright holders of
+that material) supplement the terms of this License with terms:
+
+ a) Disclaiming warranty or limiting liability differently from the
+ terms of sections 15 and 16 of this License; or
+
+ b) Requiring preservation of specified reasonable legal notices or
+ author attributions in that material or in the Appropriate Legal
+ Notices displayed by works containing it; or
+
+ c) Prohibiting misrepresentation of the origin of that material, or
+ requiring that modified versions of such material be marked in
+ reasonable ways as different from the original version; or
+
+ d) Limiting the use for publicity purposes of names of licensors or
+ authors of the material; or
+
+ e) Declining to grant rights under trademark law for use of some
+ trade names, trademarks, or service marks; or
+
+ f) Requiring indemnification of licensors and authors of that
+ material by anyone who conveys the material (or modified versions of
+ it) with contractual assumptions of liability to the recipient, for
+ any liability that these contractual assumptions directly impose on
+ those licensors and authors.
+
+ All other non-permissive additional terms are considered "further
+restrictions" within the meaning of section 10. If the Program as you
+received it, or any part of it, contains a notice stating that it is
+governed by this License along with a term that is a further
+restriction, you may remove that term. If a license document contains
+a further restriction but permits relicensing or conveying under this
+License, you may add to a covered work material governed by the terms
+of that license document, provided that the further restriction does
+not survive such relicensing or conveying.
+
+ If you add terms to a covered work in accord with this section, you
+must place, in the relevant source files, a statement of the
+additional terms that apply to those files, or a notice indicating
+where to find the applicable terms.
+
+ Additional terms, permissive or non-permissive, may be stated in the
+form of a separately written license, or stated as exceptions;
+the above requirements apply either way.
+
+ 8. Termination.
+
+ You may not propagate or modify a covered work except as expressly
+provided under this License. Any attempt otherwise to propagate or
+modify it is void, and will automatically terminate your rights under
+this License (including any patent licenses granted under the third
+paragraph of section 11).
+
+ However, if you cease all violation of this License, then your
+license from a particular copyright holder is reinstated (a)
+provisionally, unless and until the copyright holder explicitly and
+finally terminates your license, and (b) permanently, if the copyright
+holder fails to notify you of the violation by some reasonable means
+prior to 60 days after the cessation.
+
+ Moreover, your license from a particular copyright holder is
+reinstated permanently if the copyright holder notifies you of the
+violation by some reasonable means, this is the first time you have
+received notice of violation of this License (for any work) from that
+copyright holder, and you cure the violation prior to 30 days after
+your receipt of the notice.
+
+ Termination of your rights under this section does not terminate the
+licenses of parties who have received copies or rights from you under
+this License. If your rights have been terminated and not permanently
+reinstated, you do not qualify to receive new licenses for the same
+material under section 10.
+
+ 9. Acceptance Not Required for Having Copies.
+
+ You are not required to accept this License in order to receive or
+run a copy of the Program. Ancillary propagation of a covered work
+occurring solely as a consequence of using peer-to-peer transmission
+to receive a copy likewise does not require acceptance. However,
+nothing other than this License grants you permission to propagate or
+modify any covered work. These actions infringe copyright if you do
+not accept this License. Therefore, by modifying or propagating a
+covered work, you indicate your acceptance of this License to do so.
+
+ 10. Automatic Licensing of Downstream Recipients.
+
+ Each time you convey a covered work, the recipient automatically
+receives a license from the original licensors, to run, modify and
+propagate that work, subject to this License. You are not responsible
+for enforcing compliance by third parties with this License.
+
+ An "entity transaction" is a transaction transferring control of an
+organization, or substantially all assets of one, or subdividing an
+organization, or merging organizations. If propagation of a covered
+work results from an entity transaction, each party to that
+transaction who receives a copy of the work also receives whatever
+licenses to the work the party's predecessor in interest had or could
+give under the previous paragraph, plus a right to possession of the
+Corresponding Source of the work from the predecessor in interest, if
+the predecessor has it or can get it with reasonable efforts.
+
+ You may not impose any further restrictions on the exercise of the
+rights granted or affirmed under this License. For example, you may
+not impose a license fee, royalty, or other charge for exercise of
+rights granted under this License, and you may not initiate litigation
+(including a cross-claim or counterclaim in a lawsuit) alleging that
+any patent claim is infringed by making, using, selling, offering for
+sale, or importing the Program or any portion of it.
+
+ 11. Patents.
+
+ A "contributor" is a copyright holder who authorizes use under this
+License of the Program or a work on which the Program is based. The
+work thus licensed is called the contributor's "contributor version".
+
+ A contributor's "essential patent claims" are all patent claims
+owned or controlled by the contributor, whether already acquired or
+hereafter acquired, that would be infringed by some manner, permitted
+by this License, of making, using, or selling its contributor version,
+but do not include claims that would be infringed only as a
+consequence of further modification of the contributor version. For
+purposes of this definition, "control" includes the right to grant
+patent sublicenses in a manner consistent with the requirements of
+this License.
+
+ Each contributor grants you a non-exclusive, worldwide, royalty-free
+patent license under the contributor's essential patent claims, to
+make, use, sell, offer for sale, import and otherwise run, modify and
+propagate the contents of its contributor version.
+
+ In the following three paragraphs, a "patent license" is any express
+agreement or commitment, however denominated, not to enforce a patent
+(such as an express permission to practice a patent or covenant not to
+sue for patent infringement). To "grant" such a patent license to a
+party means to make such an agreement or commitment not to enforce a
+patent against the party.
+
+ If you convey a covered work, knowingly relying on a patent license,
+and the Corresponding Source of the work is not available for anyone
+to copy, free of charge and under the terms of this License, through a
+publicly available network server or other readily accessible means,
+then you must either (1) cause the Corresponding Source to be so
+available, or (2) arrange to deprive yourself of the benefit of the
+patent license for this particular work, or (3) arrange, in a manner
+consistent with the requirements of this License, to extend the patent
+license to downstream recipients. "Knowingly relying" means you have
+actual knowledge that, but for the patent license, your conveying the
+covered work in a country, or your recipient's use of the covered work
+in a country, would infringe one or more identifiable patents in that
+country that you have reason to believe are valid.
+
+ If, pursuant to or in connection with a single transaction or
+arrangement, you convey, or propagate by procuring conveyance of, a
+covered work, and grant a patent license to some of the parties
+receiving the covered work authorizing them to use, propagate, modify
+or convey a specific copy of the covered work, then the patent license
+you grant is automatically extended to all recipients of the covered
+work and works based on it.
+
+ A patent license is "discriminatory" if it does not include within
+the scope of its coverage, prohibits the exercise of, or is
+conditioned on the non-exercise of one or more of the rights that are
+specifically granted under this License. You may not convey a covered
+work if you are a party to an arrangement with a third party that is
+in the business of distributing software, under which you make payment
+to the third party based on the extent of your activity of conveying
+the work, and under which the third party grants, to any of the
+parties who would receive the covered work from you, a discriminatory
+patent license (a) in connection with copies of the covered work
+conveyed by you (or copies made from those copies), or (b) primarily
+for and in connection with specific products or compilations that
+contain the covered work, unless you entered into that arrangement,
+or that patent license was granted, prior to 28 March 2007.
+
+ Nothing in this License shall be construed as excluding or limiting
+any implied license or other defenses to infringement that may
+otherwise be available to you under applicable patent law.
+
+ 12. No Surrender of Others' Freedom.
+
+ If 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 convey a
+covered work so as to satisfy simultaneously your obligations under this
+License and any other pertinent obligations, then as a consequence you may
+not convey it at all. For example, if you agree to terms that obligate you
+to collect a royalty for further conveying from those to whom you convey
+the Program, the only way you could satisfy both those terms and this
+License would be to refrain entirely from conveying the Program.
+
+ 13. Use with the GNU Affero General Public License.
+
+ Notwithstanding any other provision of this License, you have
+permission to link or combine any covered work with a work licensed
+under version 3 of the GNU Affero General Public License into a single
+combined work, and to convey the resulting work. The terms of this
+License will continue to apply to the part which is the covered work,
+but the special requirements of the GNU Affero General Public License,
+section 13, concerning interaction through a network will apply to the
+combination as such.
+
+ 14. Revised Versions of this License.
+
+ The Free Software Foundation may publish revised and/or new versions of
+the GNU 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 that a certain numbered version of the GNU General
+Public License "or any later version" applies to it, you have the
+option of following the terms and conditions either of that numbered
+version or of any later version published by the Free Software
+Foundation. If the Program does not specify a version number of the
+GNU General Public License, you may choose any version ever published
+by the Free Software Foundation.
+
+ If the Program specifies that a proxy can decide which future
+versions of the GNU General Public License can be used, that proxy's
+public statement of acceptance of a version permanently authorizes you
+to choose that version for the Program.
+
+ Later license versions may give you additional or different
+permissions. However, no additional obligations are imposed on any
+author or copyright holder as a result of your choosing to follow a
+later version.
+
+ 15. Disclaimer of Warranty.
+
+ 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.
+
+ 16. Limitation of Liability.
+
+ IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
+WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS
+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.
+
+ 17. Interpretation of Sections 15 and 16.
+
+ If the disclaimer of warranty and limitation of liability provided
+above cannot be given local legal effect according to their terms,
+reviewing courts shall apply local law that most closely approximates
+an absolute waiver of all civil liability in connection with the
+Program, unless a warranty or assumption of liability accompanies a
+copy of the Program in return for a fee.
+
+ 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
+state the exclusion of warranty; and each file should have at least
+the "copyright" line and a pointer to where the full notice is found.
+
+
+ Copyright (C)
+
+ 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 3 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, see .
+
+Also add information on how to contact you by electronic and paper mail.
+
+ If the program does terminal interaction, make it output a short
+notice like this when it starts in an interactive mode:
+
+ Copyright (C)
+ This program 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, your program's commands
+might be different; for a GUI interface, you would use an "about box".
+
+ You should also get your employer (if you work as a programmer) or school,
+if any, to sign a "copyright disclaimer" for the program, if necessary.
+For more information on this, and how to apply and follow the GNU GPL, see
+.
+
+ The GNU 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. But first, please read
+.
diff --git a/ubo-modsperson/pom.xml b/ubo-modsperson/pom.xml
new file mode 100644
index 000000000..74e13de56
--- /dev/null
+++ b/ubo-modsperson/pom.xml
@@ -0,0 +1,100 @@
+
+
+
+ 4.0.0
+
+
+ org.mycore.ubo
+ ubo
+ dev-SNAPSHOT
+
+
+ ubo-modsperson
+ Universitätsbibliographie - Person Module
+
+
+
+
+ org.apache.maven.plugins
+ maven-jar-plugin
+
+
+
+ true
+
+
+
+ ${project.artifactId}
+ ${project.name}
+ 50
+
+
+
+
+
+ org.mycore.plugins
+ datamodel-plugin
+ 0.8
+
+
+
+ schema
+
+
+
+
+
+
+
+
+
+ jakarta.servlet
+ jakarta.servlet-api
+
+
+ org.apache.commons
+ commons-lang3
+
+
+ org.apache.logging.log4j
+ log4j-api
+
+
+ org.jdom
+ jdom2
+
+
+ org.mycore
+ mycore-base
+
+
+ org.mycore
+ mycore-mods
+
+
+ org.mycore
+ mycore-orcid2
+
+
+ org.mycore
+ mycore-solr
+
+
+ com.h2database
+ h2
+ test
+
+
+ junit
+ junit
+ test
+
+
+ org.mycore
+ mycore-base
+ test-jar
+ test
+
+
+
+
diff --git a/ubo-modsperson/src/main/datamodel/def/modsperson.xml b/ubo-modsperson/src/main/datamodel/def/modsperson.xml
new file mode 100644
index 000000000..fd2ea335a
--- /dev/null
+++ b/ubo-modsperson/src/main/datamodel/def/modsperson.xml
@@ -0,0 +1,14 @@
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/ubo-modsperson/src/main/java/org/mycore/ubo/modsperson/MODSPersonCachingStartupHandler.java b/ubo-modsperson/src/main/java/org/mycore/ubo/modsperson/MODSPersonCachingStartupHandler.java
new file mode 100644
index 000000000..b0a5a5124
--- /dev/null
+++ b/ubo-modsperson/src/main/java/org/mycore/ubo/modsperson/MODSPersonCachingStartupHandler.java
@@ -0,0 +1,136 @@
+package org.mycore.ubo.modsperson;
+
+import org.apache.logging.log4j.LogManager;
+import org.apache.logging.log4j.Logger;
+import org.apache.solr.client.solrj.SolrQuery;
+import org.apache.solr.client.solrj.SolrServerException;
+import org.apache.solr.client.solrj.impl.BaseHttpSolrClient;
+import org.apache.solr.client.solrj.response.QueryResponse;
+import org.apache.solr.common.SolrDocument;
+import org.apache.solr.common.SolrDocumentList;
+import org.mycore.common.events.MCRStartupHandler;
+import org.mycore.datamodel.metadata.MCRObjectID;
+import org.mycore.solr.MCRSolrCoreManager;
+
+import java.io.IOException;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.concurrent.atomic.AtomicInteger;
+import java.util.stream.Collectors;
+
+/**
+ * Loads all modsperson data into {@link MODSPersonLookup} on startup.
+ */
+public class MODSPersonCachingStartupHandler implements MCRStartupHandler.AutoExecutable {
+
+ private final static Logger LOGGER = LogManager.getLogger();
+
+ @Override
+ public String getName() {
+ return "MODSPersonCachingStartupHandler";
+ }
+
+ @Override
+ public int getPriority() {
+ return 0;
+ }
+
+
+ @Override
+ public void startUp(jakarta.servlet.ServletContext servletContext) {
+ if (servletContext == null) { // only run in WebApp
+ return;
+ }
+
+ AtomicInteger counter = new AtomicInteger(0);
+ int offset = 0;
+ int blockSize = 1000;
+
+ final SolrQuery query = new SolrQuery("objectType:modsperson")
+ .setFields("*").setRows(blockSize).setParam("wt", "json");
+
+ try {
+ int resultsFound = fetchAndProcessResults(query, offset, counter);
+ if (resultsFound > blockSize) {
+ for (int i = 0; i < resultsFound / blockSize; i++) {
+ offset += 1000;
+ fetchAndProcessResults(query, offset, counter);
+ }
+ }
+ } catch (SolrServerException | BaseHttpSolrClient.RemoteSolrException | IOException e) {
+ LOGGER.error(e.getMessage());
+ }
+ LOGGER.info("Loaded " + counter.get() + " modsperson-objects into the cache");
+ }
+
+ private int fetchAndProcessResults(SolrQuery query, int offset, AtomicInteger counter)
+ throws SolrServerException, IOException {
+ query.setStart(offset);
+
+ QueryResponse queryResponse = MCRSolrCoreManager.getMainSolrClient().query(query);
+ SolrDocumentList results = queryResponse.getResults();
+
+ for (SolrDocument doc : results) {
+ addSolrDocToLookup(doc, counter);
+ }
+
+ return (int) results.getNumFound();
+ }
+
+ private void addSolrDocToLookup(SolrDocument doc, AtomicInteger counter) {
+ try {
+ MCRObjectID personmodsId = MCRObjectID.getInstance((String) doc.getFieldValue("id"));
+ String familyName;
+ String givenName;
+ Set keys = new HashSet<>();
+ String personName = ((List) doc.getFieldValue("person")).get(0);
+ if (personName != null && personName.contains(",")) {
+ String[] parts = personName.split(",");
+ if (parts.length > 1) {
+ familyName = parts[0].trim();
+ givenName = parts[1].trim();
+ } else {
+ familyName = parts[0].trim();
+ givenName = null;
+ }
+
+ }
+ else {
+ return;
+ }
+ for (String fieldName : doc.getFieldNames()) {
+ if (fieldName.startsWith("nid_") && !fieldName.equals("nid_connection")) {
+ List fieldValues = (List) doc.getFieldValue(fieldName);
+ for (String fieldValue : fieldValues) {
+ String idType = fieldName.substring(4);
+ keys.add(String.join("|", fieldValue.trim(), idType.trim()));
+ }
+
+ }
+ }
+
+ List alternativeNamesString = (List) doc.getFieldValue("alternative_name");
+ if (alternativeNamesString != null) {
+ Set> alternativeNames = alternativeNamesString.stream()
+ .map(name -> {
+ String[] parts = name.split(",\\s*");
+ return parts.length > 1 ? Map.entry(parts[0], parts[1]) : Map.entry(parts[0], "");
+ }).collect(Collectors.toSet());
+ MODSPersonLookup.add(new MODSPersonLookup.PersonCache(personmodsId, familyName,
+ givenName, keys, alternativeNames));
+ }
+ else {
+ MODSPersonLookup.add(new MODSPersonLookup.PersonCache(personmodsId, familyName,
+ givenName, keys, new HashSet<>()));
+ }
+ counter.incrementAndGet();
+
+ } catch (ClassCastException | NullPointerException | IndexOutOfBoundsException ex) {
+ // parsing a single faulty SolrDocument shouldn't interrupt the whole startup
+ LOGGER.warn("Error while loading modsperson " + doc.get("id") + " into cache", ex);
+ }
+ }
+
+}
diff --git a/ubo-modsperson/src/main/java/org/mycore/ubo/modsperson/MODSPersonEventHandler.java b/ubo-modsperson/src/main/java/org/mycore/ubo/modsperson/MODSPersonEventHandler.java
new file mode 100644
index 000000000..aada6ff6f
--- /dev/null
+++ b/ubo-modsperson/src/main/java/org/mycore/ubo/modsperson/MODSPersonEventHandler.java
@@ -0,0 +1,51 @@
+package org.mycore.ubo.modsperson;
+
+import org.apache.logging.log4j.LogManager;
+import org.apache.logging.log4j.Logger;
+import org.mycore.common.events.MCREvent;
+import org.mycore.common.events.MCREventHandlerBase;
+import org.mycore.datamodel.common.MCRLinkTableManager;
+import org.mycore.datamodel.metadata.MCRObject;
+import org.mycore.user2.MCRUser;
+import org.mycore.user2.MCRUserManager;
+
+import java.util.Set;
+import java.util.stream.Collectors;
+
+/**
+ * Handles updates and removals of modsperson-objects. Changes in modsperson objects need to be reflected
+ * in the {@link MODSPersonLookup MODSPersonLookup-Cache}.
+ */
+public class MODSPersonEventHandler extends MCREventHandlerBase {
+
+ private final static Logger LOGGER = LogManager.getLogger(MODSPersonEventHandler.class);
+
+ @Override
+ protected void handleObjectUpdated(MCREvent evt, MCRObject obj) {
+ if (!"modsperson".equals(obj.getId().getTypeId())) {
+ return;
+ }
+ MODSPersonLookup.update(obj);
+ LOGGER.info("Updated modsperson " + obj.getId().toString() + " in cache");
+ }
+
+ @Override
+ protected void handleObjectDeleted(MCREvent evt, MCRObject obj) {
+ if (!"modsperson".equals(obj.getId().getTypeId())) {
+ return;
+ }
+
+ String modspersonId = obj.getId().toString();
+ Set connectedUsers = MCRUserManager.getUsers("id_modsperson", modspersonId)
+ .collect(Collectors.toSet());
+ for (MCRUser user : connectedUsers) {
+ user.getAttributes().removeIf(ua -> (ua.getName().equals("id_modsperson") && ua.getValue().equals(modspersonId)));
+ MCRUserManager.updateUser(user);
+ }
+
+ MODSPersonLookup.remove(obj);
+ LOGGER.info("Deleted modsperson " + obj.getId().toString() + " from cache");
+ }
+
+}
+
diff --git a/ubo-modsperson/src/main/java/org/mycore/ubo/modsperson/MODSPersonLinkingEventHandler.java b/ubo-modsperson/src/main/java/org/mycore/ubo/modsperson/MODSPersonLinkingEventHandler.java
new file mode 100644
index 000000000..3608f8a39
--- /dev/null
+++ b/ubo-modsperson/src/main/java/org/mycore/ubo/modsperson/MODSPersonLinkingEventHandler.java
@@ -0,0 +1,240 @@
+package org.mycore.ubo.modsperson;
+
+import java.io.IOException;
+import java.util.Date;
+import java.util.Optional;
+import java.util.Set;
+import java.util.stream.Collectors;
+
+import org.apache.commons.lang3.StringUtils;
+import org.apache.logging.log4j.LogManager;
+import org.apache.logging.log4j.Logger;
+import org.jdom2.Document;
+import org.jdom2.Element;
+import org.mycore.access.MCRAccessException;
+import org.mycore.common.MCRConstants;
+import org.mycore.common.MCRException;
+import org.mycore.common.MCRPersistenceException;
+import org.mycore.common.MCRXlink;
+import org.mycore.common.config.MCRConfiguration2;
+import org.mycore.common.content.MCRJDOMContent;
+import org.mycore.common.events.MCREvent;
+import org.mycore.common.events.MCREventHandlerBase;
+import org.mycore.common.xml.MCRXMLHelper;
+import org.mycore.datamodel.common.MCRXMLMetadataManager;
+import org.mycore.datamodel.metadata.MCRMetadataManager;
+import org.mycore.datamodel.metadata.MCRObject;
+import org.mycore.datamodel.metadata.MCRObjectID;
+import org.mycore.mods.MCRMODSSorter;
+import org.mycore.mods.MCRMODSWrapper;
+import org.mycore.mods.merger.MCRMergeTool;
+import org.mycore.mods.merger.MCRMerger;
+import org.mycore.mods.merger.MCRMergerFactory;
+import org.mycore.ubo.modsperson.merger.MCRNameMerger;
+
+import static org.mycore.ubo.modsperson.MODSPersonUtils.hasSameNames;
+
+/**
+ * EventHandler to connect personal information in mods objects to the corresponding information in modsperson
+ * objects.
+ */
+public class MODSPersonLinkingEventHandler extends MCREventHandlerBase {
+
+ private final static Logger LOGGER = LogManager.getLogger();
+
+ private final String leadIDName;
+
+ public MODSPersonLinkingEventHandler() {
+ super();
+ this.leadIDName = MCRConfiguration2.getString("MCR.user2.matching.lead_id").orElse("");
+ }
+
+ @Override
+ protected void handleObjectCreated(MCREvent evt, MCRObject obj) {
+ boolean wasModified = handleObject(obj);
+ if (wasModified) {
+ MCRXMLMetadataManager.getInstance().update(obj.getId(), obj.createXML(), new Date());
+ }
+ }
+
+ @Override
+ protected void handleObjectUpdated(MCREvent evt, MCRObject obj) {
+ boolean wasModified = handleObject(obj);
+ if (wasModified) {
+ MCRXMLMetadataManager.getInstance().update(obj.getId(), obj.createXML(), new Date());
+ }
+ }
+
+ @Override
+ protected void handleObjectRepaired(MCREvent evt, MCRObject obj) {
+ boolean wasModified = handleObject(obj);
+ if (wasModified) {
+ MCRXMLMetadataManager.getInstance().update(obj.getId(), obj.createXML(), new Date());
+ }
+ }
+
+ protected boolean handleObject(MCRObject obj) {
+ if (!"mods".equals(obj.getId().getTypeId())) {
+ return false;
+ }
+
+ MCRMODSWrapper wrapper = new MCRMODSWrapper(obj);
+ final String publicationId = obj.getId().toString();
+ wrapper.getElements("mods:name[@type='personal']").forEach(modsName -> {
+
+ MCRObject person = getPersonReferencedIn(modsName);
+ if (person == null) {
+ person = findPersonMatching(modsName, publicationId);
+ }
+
+ boolean isNewPerson = false;
+ if ((person == null) && leadIDExists(modsName)) {
+ person = buildNewPerson();
+ isNewPerson = true;
+ }
+
+ if (person != null) {
+ mergeDataFromNameToPerson(modsName, person);
+ setReferencedPerson(modsName, person);
+ // once a modsperson is successfully created, lead-id will no longer be necessary
+ removeLeadId(modsName);
+ if (isNewPerson) {
+ MODSPersonLookup.add(person);
+ }
+ }
+ });
+
+ return wasModified(wrapper.getMODS());
+ }
+
+ private boolean wasModified(Element mods) {
+ boolean modified = mods.removeAttribute("modified");
+ LOGGER.debug("Publication was modified? " + modified);
+ return modified;
+ }
+
+ private MCRObject getPersonReferencedIn(Element modsName) {
+ String personID = modsName.getAttributeValue(MCRXlink.HREF, MCRConstants.XLINK_NAMESPACE);
+ if (!StringUtils.isEmpty(personID)) {
+ LOGGER.debug("Retrieving person object already referenced in publication: " + personID);
+ MCRObjectID oid = MCRObjectID.getInstance(personID);
+
+ try {
+ return MCRMetadataManager.retrieveMCRObject(oid);
+ } catch (MCRPersistenceException ex) {
+ LOGGER.warn("Modsperson object " + personID + " not found, remove reference link: " + ex.getMessage());
+ modsName.removeAttribute(MCRXlink.HREF, MCRConstants.XLINK_NAMESPACE);
+ }
+ }
+ return null;
+ }
+
+ private void setReferencedPerson(Element modsName, MCRObject person) {
+ if (modsName.getAttribute(MCRXlink.HREF, MCRConstants.XLINK_NAMESPACE) == null) {
+ String personID = person.getId().toString();
+ LOGGER.info("Linking mods:name in publication to " + personID);
+ modsName.setAttribute(MCRXlink.HREF, personID, MCRConstants.XLINK_NAMESPACE);
+ modsName.getParentElement().setAttribute("modified", "true");
+ MCRMODSSorter.sort(modsName);
+ }
+ }
+
+ private void removeLeadId(Element modsName) {
+ Optional leadIdOpt = modsName.getChildren("nameIdentifier", MCRConstants.MODS_NAMESPACE)
+ .stream().filter(ni -> leadIDName.equals(ni.getAttributeValue("type")))
+ .findFirst();
+ leadIdOpt.ifPresent(Element::detach);
+ }
+
+ private MCRObject findPersonMatching(Element modsName, String publicationID) {
+ Set cachedPersons = MODSPersonLookup.lookup(modsName);
+ if (cachedPersons == null || cachedPersons.isEmpty()) {
+ return null;
+ }
+
+ if (cachedPersons.size() == 1) {
+ MODSPersonLookup.PersonCache only = cachedPersons.iterator().next();
+ return MCRMetadataManager.retrieveMCRObject(only.getPersonmodsId());
+ }
+
+ for (MODSPersonLookup.PersonCache pc : cachedPersons) {
+ if (hasSameNames(modsName, pc)) {
+ return MCRMetadataManager.retrieveMCRObject(pc.getPersonmodsId());
+ }
+ }
+
+ // Fallback
+ MODSPersonLookup.PersonCache firstMatch = cachedPersons.iterator().next();
+ String allIDs = cachedPersons.stream()
+ .map(o -> o.getPersonmodsId().toString()).collect(Collectors.joining(", "));
+
+ LOGGER.warn("There are multiple modsperson-objects matching the person in publication "
+ + publicationID + ": [" + allIDs + "]. No name-match found, chosing "
+ + firstMatch.getPersonmodsId().toString() + ".");
+
+ return MCRMetadataManager.retrieveMCRObject(firstMatch.getPersonmodsId());
+ }
+
+ private boolean leadIDExists(Element modsName) {
+ return modsName.getChildren("nameIdentifier", MCRConstants.MODS_NAMESPACE)
+ .stream().anyMatch(ni -> leadIDName.equals(ni.getAttributeValue("type")));
+ }
+
+ private MCRObject buildNewPerson() {
+ LOGGER.debug("Creating new person object...");
+ return new MCRObject(new Document(MODSPersonUtils.getMODSPersonTemplate().clone()));
+ }
+
+ /**
+ * Writes additional data from mods name object into the modsperson object.
+ * @param modsName the {@link Element} which contains information about the person
+ * @param person the modsperson object that should be enriched with additional data
+ */
+ private void mergeDataFromNameToPerson(Element modsName, MCRObject person) {
+ LOGGER.info("Merging data from mods:name in publication to person object...");
+
+ MCRMODSWrapper wrapper = new MCRMODSWrapper(person);
+
+ Element personName = wrapper.getElement("mods:name[@type='personal']");
+ if (personName != null) {
+ Element personOld = personName.clone();
+
+ MCRMergeTool.merge(personName, filterMODS(modsName));
+
+ // build an alternativeName, if names are not exactly the same
+ MCRMerger mergeInto = MCRMergerFactory.buildFrom(personName);
+ MCRMerger mergeFrom = MCRMergerFactory.buildFrom(modsName);
+ if (mergeInto.isProbablySameAs(mergeFrom)) {
+ ((MCRNameMerger) mergeInto).mergeAsAlternativeName(mergeFrom);
+ }
+
+ MCRMODSSorter.sort(wrapper.getMODS());
+ MCRMODSSorter.sort(personName);
+
+ if (!MCRXMLHelper.deepEqual(personName, personOld)) {
+ LOGGER.info("Person object is changed after merge, saving...");
+ try {
+ if (LOGGER.isDebugEnabled()) {
+ LOGGER.debug(new MCRJDOMContent(personName.clone()).asString());
+ }
+ MCRMetadataManager.update(person);
+ } catch (MCRPersistenceException | MCRAccessException | IOException ex) {
+ throw new MCRException(ex);
+ }
+ } else {
+ LOGGER.info("Person object is unchanged after merge, nothing to do.");
+ }
+ }
+ }
+
+ private Element filterMODS(Element modsName) {
+ Element cloned = modsName.clone();
+ cloned.removeAttribute(MCRXlink.HREF, MCRConstants.XLINK_NAMESPACE);
+ cloned.removeChildren("displayForm", MCRConstants.MODS_NAMESPACE);
+ cloned.removeChildren("alternativeName", MCRConstants.MODS_NAMESPACE);
+ cloned.removeChildren("role", MCRConstants.MODS_NAMESPACE);
+ cloned.removeChildren("description", MCRConstants.MODS_NAMESPACE);
+ cloned.removeChildren("etal", MCRConstants.MODS_NAMESPACE);
+ return cloned;
+ }
+}
diff --git a/ubo-modsperson/src/main/java/org/mycore/ubo/modsperson/MODSPersonLookup.java b/ubo-modsperson/src/main/java/org/mycore/ubo/modsperson/MODSPersonLookup.java
new file mode 100644
index 000000000..38ac87010
--- /dev/null
+++ b/ubo-modsperson/src/main/java/org/mycore/ubo/modsperson/MODSPersonLookup.java
@@ -0,0 +1,210 @@
+package org.mycore.ubo.modsperson;
+
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.stream.Collectors;
+
+import org.jdom2.Element;
+import org.mycore.common.MCRConstants;
+import org.mycore.datamodel.metadata.MCRObject;
+import org.mycore.datamodel.metadata.MCRObjectID;
+import org.mycore.mods.MCRMODSWrapper;
+
+import static org.mycore.ubo.modsperson.MODSPersonUtils.hasSameNames;
+
+public class MODSPersonLookup {
+
+ private static final Map> nameId2person = new HashMap<>();
+
+ private static final Map id2person = new HashMap<>();
+
+ public static void add(MCRObject person) {
+ Element mods = new MCRMODSWrapper(person).getMODS().clone();
+ PersonCache personCache = PersonCache.newPersonCacheFromMCRObject(person);
+ if (personCache != null) {
+ id2person.put(person.getId().toString(), personCache);
+ getNameIdentifiers(mods).forEach(nameID -> {
+ String key = buildKey(nameID);
+ nameId2person.computeIfAbsent(key, k -> new HashSet<>()).add(personCache);
+ });
+ }
+ }
+
+ public static void add(PersonCache personCache) {
+ id2person.put(personCache.getPersonmodsId().toString(), personCache);
+ personCache.getKeys().forEach(key -> {
+ nameId2person.computeIfAbsent(key, k -> new HashSet<>()).add(personCache);
+ });
+ }
+
+ public static void remove(MCRObject person) {
+ Element mods = new MCRMODSWrapper(person).getMODS().clone();
+ id2person.remove(person.getId().toString());
+
+ getNameIdentifiers(mods).forEach(nameID -> {
+ String key = buildKey(nameID);
+ Set personSet = nameId2person.get(key);
+ if (personSet != null) {
+ personSet.removeIf(testedPerson -> testedPerson.getPersonmodsId().equals(person.getId()));
+ if (personSet.isEmpty()) {
+ nameId2person.remove(key);
+ }
+ }
+ });
+ }
+
+ /**
+ * Fully empties the lookup. Use with caution!
+ */
+ public static void clear() {
+ id2person.clear();
+ nameId2person.clear();
+ }
+
+ public static void update(MCRObject person) {
+ remove(person);
+ add(person);
+ }
+
+ private static String buildKey(Element nameIdentifier) {
+ return String.join("|", nameIdentifier.getTextTrim(), nameIdentifier.getAttributeValue("type"));
+ }
+
+ private static List getNameIdentifiers(Element mods) {
+ if (mods != null && mods.getChild("name", MCRConstants.MODS_NAMESPACE) != null) {
+ return mods.getChild("name", MCRConstants.MODS_NAMESPACE).getChildren("nameIdentifier",
+ MCRConstants.MODS_NAMESPACE);
+ }
+ return new ArrayList<>();
+ }
+
+ public static Set lookup(Element modsName) {
+ Set keys2lookup = modsName.getChildren("nameIdentifier", MCRConstants.MODS_NAMESPACE)
+ .stream().map(MODSPersonLookup::buildKey).collect(Collectors.toSet());
+
+ keys2lookup.retainAll(nameId2person.keySet());
+
+ keys2lookup.removeIf(key -> nameId2person.get(key) == null || nameId2person.get(key).stream()
+ .filter(savedPerson -> savedPerson == null || !hasSameNames(modsName, savedPerson))
+ .count() == nameId2person.get(key).size());
+ return keys2lookup.isEmpty() ? null : nameId2person.get(keys2lookup.iterator().next());
+ }
+
+ /**
+ * @return gets number of modsperson-objects in cache by modsperson-ID
+ */
+ public static int getCacheSize() {
+ return id2person.size();
+ }
+
+ public static class PersonCache {
+
+ public PersonCache(MCRObjectID personmodsId, String familyName, String givenName, Set keys,
+ Set> alternativeNames) {
+ this.personmodsId = personmodsId;
+ this.familyName = familyName;
+ this.givenName = givenName;
+ this.keys = keys;
+ this.alternativeNames = alternativeNames;
+ }
+
+ private MCRObjectID personmodsId;
+ private String familyName;
+ private String givenName;
+ private Set keys;
+
+ // in the form: Entry
+ private Set> alternativeNames;
+
+ public static PersonCache newPersonCacheFromMCRObject(MCRObject obj) {
+ MCRMODSWrapper wrapper = new MCRMODSWrapper(obj);
+ Element modsName = wrapper.getMODS().getChild("name", MCRConstants.MODS_NAMESPACE);
+ if (modsName == null) {
+ return null;
+ }
+
+ String familyNameString = modsName
+ .getChildren().stream().filter(e -> "family".equals(e.getAttributeValue("type")))
+ .map(Element::getText).findFirst().orElse("").trim();
+
+ String givenNameString = modsName
+ .getChildren().stream().filter(e -> "given".equals(e.getAttributeValue("type")))
+ .map(Element::getText).findFirst().orElse("").trim();
+
+ Set allKeys = modsName
+ .getChildren("nameIdentifier", MCRConstants.MODS_NAMESPACE)
+ .stream().map(MODSPersonLookup::buildKey).collect(Collectors.toSet());
+
+ Set> alternativeNamesLocal = new HashSet<>();
+
+ modsName.getChildren("alternativeName", MCRConstants.MODS_NAMESPACE).forEach(alternativeName -> {
+ String altFamilyNameLocal = alternativeName.getChildren().stream()
+ .filter(e -> "family".equals(e.getAttributeValue("type")))
+ .map(Element::getText).findFirst().orElse("");
+
+ String altGivenNameLocal = alternativeName.getChildren().stream()
+ .filter(e -> "given".equals(e.getAttributeValue("type")))
+ .map(Element::getText).findFirst().orElse("");
+
+
+ alternativeNamesLocal.add(Map.entry(altFamilyNameLocal, altGivenNameLocal));
+
+ });
+
+ return new PersonCache(obj.getId(), familyNameString, givenNameString, allKeys, alternativeNamesLocal);
+ }
+
+ public MCRObjectID getPersonmodsId() {
+ return personmodsId;
+ }
+
+ public void setPersonmodsId(MCRObjectID personmodsId) {
+ this.personmodsId = personmodsId;
+ }
+
+ public String getFamilyName() {
+ return familyName;
+ }
+
+ public void setFamilyName(String familyName) {
+ this.familyName = familyName;
+ }
+
+ public String getGivenName() {
+ return givenName;
+ }
+
+ public void setGivenName(String givenName) {
+ this.givenName = givenName;
+ }
+
+ public Set getKeys() {
+ return keys;
+ }
+
+ public void setKeys(Set keys) {
+ this.keys = keys;
+ }
+
+ public void addKey(String key) {
+ this.keys.add(key);
+ }
+
+ public Set> getAlternativeNames() {
+ return alternativeNames;
+ }
+
+ public void setAlternativeNames(Set> alternativeNames) {
+ this.alternativeNames = alternativeNames;
+ }
+
+ public void addAlternativeNames(Map.Entry alternativeName) {
+ alternativeNames.add(alternativeName);
+ }
+ }
+
+}
diff --git a/ubo-modsperson/src/main/java/org/mycore/ubo/modsperson/MODSPersonUtils.java b/ubo-modsperson/src/main/java/org/mycore/ubo/modsperson/MODSPersonUtils.java
new file mode 100644
index 000000000..7a68e5085
--- /dev/null
+++ b/ubo-modsperson/src/main/java/org/mycore/ubo/modsperson/MODSPersonUtils.java
@@ -0,0 +1,98 @@
+package org.mycore.ubo.modsperson;
+
+import org.jdom2.Element;
+import org.mycore.common.MCRConstants;
+import org.mycore.common.config.MCRConfiguration2;
+import org.mycore.datamodel.metadata.MCRObjectID;
+import org.mycore.mods.merger.MCRMerger;
+import org.mycore.mods.merger.MCRMergerFactory;
+import org.mycore.ubo.modsperson.merger.MCRNameMerger;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+import java.util.Map;
+
+public class MODSPersonUtils {
+
+ private final static Element PERSON_TEMPLATE;
+
+ static {
+ String projectID = MCRConfiguration2.getStringOrThrow("UBO.projectid.default");
+ String defaultID = MCRObjectID.formatID(projectID, "modsperson", 0);
+
+ Element modsName = new Element("name", MCRConstants.MODS_NAMESPACE).setAttribute("type", "personal");
+ Element mods = new Element("mods", MCRConstants.MODS_NAMESPACE).addContent(modsName);
+ Element mc = new Element("modsContainer").addContent(mods);
+ Element dmc = new Element("def.modsContainer").setAttribute("class", "MCRMetaXML").addContent(mc);
+
+ Element mycoreobject = new Element("mycoreobject");
+ mycoreobject.setAttribute("ID", defaultID);
+ mycoreobject.setAttribute("noNamespaceSchemaLocation", "datamodel-modsperson.xsd", MCRConstants.XSI_NAMESPACE);
+ mycoreobject.addContent(new Element("structure"));
+ mycoreobject.addContent(new Element("metadata").addContent(dmc));
+ mycoreobject.addContent(new Element("service"));
+
+ PERSON_TEMPLATE = mycoreobject;
+ }
+
+ public static Element getMODSPersonTemplate() {
+ return PERSON_TEMPLATE;
+ }
+
+ /**
+ * Compares a modsName Element with a given cached person for same names.
+ * @param elementToCompare modsName Element to compare to person in cache
+ * @param cache Element from cache that is compared
+ * @return true if the names are the same or if the cache has an alternative name matching the elementToCompare
+ */
+ protected static boolean hasSameNames(Element elementToCompare, MODSPersonLookup.PersonCache cache) {
+ Element savedElement = personCache2ModsName(cache);
+
+ MCRMerger merger1 = MCRMergerFactory.buildFrom(elementToCompare);
+ MCRMerger merger2 = MCRMergerFactory.buildFrom(savedElement);
+ return merger1.isProbablySameAs(merger2) || ((MCRNameMerger) merger2).hasAlternativeNameSameAs(merger1);
+ }
+
+ /**
+ * Creates a mods:name Element containing only names. Util method for name comparison. Element
+ * contains family name, given name and potential alternative names.
+ * @param cache the {@link MODSPersonLookup.PersonCache} to compare.
+ * @return the Element with all names
+ */
+ protected static Element personCache2ModsName(MODSPersonLookup.PersonCache cache) {
+ Element newModsName = new Element("name", MCRConstants.MODS_NAMESPACE);
+ newModsName.setAttribute("type", "personal");
+
+ List innerNameElements = new ArrayList<>();
+
+ Element familyNameElement = new Element("namePart", MCRConstants.MODS_NAMESPACE);
+ familyNameElement.setAttribute("type", "family");
+ familyNameElement.setText(cache.getFamilyName());
+ innerNameElements.add(familyNameElement);
+
+ Element givenNameElement = new Element("namePart", MCRConstants.MODS_NAMESPACE);
+ givenNameElement.setAttribute("type", "given");
+ givenNameElement.setText(cache.getGivenName());
+ innerNameElements.add(givenNameElement);
+
+
+ for (Map.Entry entry : cache.getAlternativeNames()) {
+ Element altNameElement = new Element("alternativeName", MCRConstants.MODS_NAMESPACE);
+
+ Element altFamilyNameElement = new Element("namePart", MCRConstants.MODS_NAMESPACE);
+ altFamilyNameElement.setAttribute("type", "family");
+ altFamilyNameElement.setText(entry.getKey());
+
+ Element altGivenNameElement = new Element("namePart", MCRConstants.MODS_NAMESPACE);
+ altGivenNameElement.setAttribute("type", "given");
+ altGivenNameElement.setText(entry.getValue());
+
+ altNameElement.setContent(Arrays.asList(altFamilyNameElement, altGivenNameElement));
+ innerNameElements.add(altNameElement);
+ }
+
+ newModsName.setContent(innerNameElements);
+ return newModsName;
+ }
+}
diff --git a/ubo-modsperson/src/main/java/org/mycore/ubo/modsperson/linking/MODSPersonORCIDLinkingEventHandler.java b/ubo-modsperson/src/main/java/org/mycore/ubo/modsperson/linking/MODSPersonORCIDLinkingEventHandler.java
new file mode 100644
index 000000000..0aff211ac
--- /dev/null
+++ b/ubo-modsperson/src/main/java/org/mycore/ubo/modsperson/linking/MODSPersonORCIDLinkingEventHandler.java
@@ -0,0 +1,139 @@
+package org.mycore.ubo.modsperson.linking;
+
+import org.apache.logging.log4j.LogManager;
+import org.apache.logging.log4j.Logger;
+import org.jdom2.Element;
+import org.mycore.access.MCRAccessException;
+import org.mycore.common.MCRException;
+import org.mycore.common.MCRPersistenceException;
+import org.mycore.common.events.MCREvent;
+import org.mycore.common.events.MCREventHandler;
+import org.mycore.datamodel.metadata.MCRMetadataManager;
+import org.mycore.datamodel.metadata.MCRObject;
+import org.mycore.datamodel.metadata.MCRObjectID;
+import org.mycore.mods.MCRMODSWrapper;
+import org.mycore.user2.MCRUser;
+import org.mycore.user2.MCRUserAttribute;
+
+import java.util.List;
+import java.util.Set;
+import java.util.stream.Collectors;
+
+import static org.mycore.common.MCRConstants.MODS_NAMESPACE;
+
+/**
+ * For each change in an {@link MCRUser}, checks if the user is an authenticated ORCID-user AND
+ * if the user is linked to a modsperson (owns the attribute "id_modsperson"). If so, and if
+ * the authenticated ORCID-ID is not yet known to the modsperson, copy it over from the user.
+ */
+public class MODSPersonORCIDLinkingEventHandler implements MCREventHandler {
+
+ private final static Logger LOGGER = LogManager.getLogger(MODSPersonORCIDLinkingEventHandler.class);
+
+ private final static String ID_MODSPERSON = "id_modsperson";
+ private final static String ORCID_PREFIX = "orcid_credential_";
+ private final static String ORCID_TYPE = "orcid";
+ private final static String TYPE_ATTRIBUTE = "type";
+
+ private enum Action {ADD, REMOVE}
+
+ @Override
+ public void doHandleEvent(MCREvent mcrEvent) throws MCRException {
+ processUserEvent(mcrEvent, Action.ADD);
+ }
+
+ @Override
+ public void undoHandleEvent(MCREvent mcrEvent) throws MCRException {
+ processUserEvent(mcrEvent, Action.REMOVE);
+ }
+
+ private void processUserEvent(MCREvent mcrEvent, Action action) {
+ if (MCREvent.ObjectType.USER != mcrEvent.getObjectType()) {
+ return;
+ }
+ MCRUser user = (MCRUser) mcrEvent.get(MCREvent.USER_KEY);
+ if (user == null) {
+ return;
+ }
+ Set attributes = user.getAttributes();
+
+ String modspersonId = attributes.stream()
+ .filter(a -> ID_MODSPERSON.equals(a.getName()))
+ .map(MCRUserAttribute::getValue)
+ .findFirst()
+ .orElse(null);
+ if (modspersonId == null) {
+ return;
+ }
+
+ List orcidSuffixes = attributes.stream()
+ .map(MCRUserAttribute::getName)
+ .filter(n -> n.startsWith(ORCID_PREFIX))
+ .map(n -> n.substring(ORCID_PREFIX.length()))
+ .filter(s -> !s.isEmpty())
+ .toList();
+ if (orcidSuffixes.isEmpty()) {
+ return;
+ }
+
+ switch (mcrEvent.getEventType()) {
+ case CREATE, UPDATE -> {
+ modifyOrcidAttributes(orcidSuffixes, modspersonId, user.getUserName(), action);
+ }
+ }
+ }
+
+ private void modifyOrcidAttributes(List orcidSuffixes, String modspersonId,
+ String userName, Action action) {
+ try {
+ MCRObject modsperson = MCRMetadataManager.retrieveMCRObject(MCRObjectID.getInstance(modspersonId));
+ MCRMODSWrapper wrapper = new MCRMODSWrapper(modsperson);
+ Element personName = wrapper.getElement("mods:name[@type='personal']");
+
+ if (Action.ADD.equals(action)) {
+ Set existing = personName.getChildren("nameIdentifier", MODS_NAMESPACE).stream()
+ .filter(e -> ORCID_TYPE.equals(e.getAttributeValue(TYPE_ATTRIBUTE)))
+ .map(Element::getText)
+ .collect(Collectors.toSet());
+
+ for (String orcidSuffix : orcidSuffixes) {
+ if (!existing.contains(orcidSuffix)) {
+ personName.addContent(new Element("nameIdentifier", MODS_NAMESPACE)
+ .setAttribute(TYPE_ATTRIBUTE, ORCID_TYPE).setText(orcidSuffix));
+ LOGGER.info("Adding the orcid-ID {} to person {}", orcidSuffix, modspersonId);
+ } else {
+ LOGGER.debug("ORCID {} already present for person {}, skipping", orcidSuffix, modspersonId);
+ }
+ }
+
+ } else if (Action.REMOVE.equals(action)) {
+ for (String orcidSuffix : orcidSuffixes) {
+ personName.getChildren("nameIdentifier", MODS_NAMESPACE).stream()
+ .filter(e -> ORCID_TYPE.equals(e.getAttributeValue(TYPE_ATTRIBUTE))
+ && orcidSuffix.equals(e.getText()))
+ .findFirst()
+ .ifPresent(personName::removeContent);
+ }
+ }
+
+ if (!orcidSuffixes.isEmpty()) {
+ MCRMetadataManager.update(modsperson);
+ }
+ } catch (MCRPersistenceException e) {
+ doPersistanceErrorLog(e, modspersonId, userName);
+ } catch (MCRAccessException e) {
+ doAccessErrorLog(e, modspersonId);
+ }
+ }
+
+ private void doPersistanceErrorLog(MCRPersistenceException e, String modspersonId, String userName) {
+ LOGGER.warn("Modsperson with id {} is referenced by user {}, but not found: "
+ , modspersonId, userName, e);
+ }
+
+ private void doAccessErrorLog(MCRAccessException e, String modspersonId) {
+ LOGGER.warn("Couldn't access Modsperson with id {}: "
+ , modspersonId, e);
+ }
+
+}
diff --git a/ubo-modsperson/src/main/java/org/mycore/ubo/modsperson/linking/UBOMODSLinksEventHandler.java b/ubo-modsperson/src/main/java/org/mycore/ubo/modsperson/linking/UBOMODSLinksEventHandler.java
new file mode 100644
index 000000000..4cd8ef59e
--- /dev/null
+++ b/ubo-modsperson/src/main/java/org/mycore/ubo/modsperson/linking/UBOMODSLinksEventHandler.java
@@ -0,0 +1,109 @@
+/*
+ * This file is part of *** M y C o R e ***
+ * See https://www.mycore.de/ for details.
+ *
+ * MyCoRe 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 3 of the License, or
+ * (at your option) any later version.
+ *
+ * MyCoRe 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 MyCoRe. If not, see .
+ */
+
+package org.mycore.ubo.modsperson.linking;
+
+import java.util.HashSet;
+import java.util.List;
+import java.util.Set;
+import java.util.stream.Stream;
+
+import org.jdom2.Element;
+import org.mycore.common.MCRConstants;
+import org.mycore.common.MCRXlink;
+import org.mycore.common.events.MCREvent;
+import org.mycore.common.events.MCREventHandlerBase;
+import org.mycore.datamodel.classifications2.MCRCategLinkReference;
+import org.mycore.datamodel.classifications2.MCRCategLinkServiceFactory;
+import org.mycore.datamodel.classifications2.MCRCategoryID;
+import org.mycore.datamodel.common.MCRLinkTableManager;
+import org.mycore.datamodel.metadata.MCRObject;
+import org.mycore.datamodel.metadata.MCRObjectID;
+import org.mycore.mods.MCRMODSWrapper;
+
+/**
+ * Eventhandler for linking MODS_OBJECTTYPE document to MyCoRe classifications.
+ * Overwrites MCRMODSLinksEventHandler from mycore-base.
+ *
+ * @author Thomas Scheffler (yagee)
+ */
+public class UBOMODSLinksEventHandler extends MCREventHandlerBase {
+
+ static final String LINKED_PERSONS = "mods:name[@type='personal' and"
+ + " string-length(substring-after(@xlink:href,'_')) > 0 and"
+ + " string-length(substring-after(substring-after(@xlink:href,'_'), '_')) > 0 and"
+ + " number(substring-after(substring-after(@xlink:href,'_'),'_')) > 0]";
+
+ /* (non-Javadoc)
+ * @see org.mycore.common.events.MCREventHandlerBase
+ * #handleObjectCreated(org.mycore.common.events.MCREvent, org.mycore.datamodel.metadata.MCRObject)
+ */
+ @Override
+ protected void handleObjectCreated(final MCREvent evt, final MCRObject obj) {
+ if (!MCRMODSWrapper.isSupported(obj)) {
+ return;
+ }
+ MCRLinkTableManager linkTableManager = MCRLinkTableManager.getInstance();
+ // delete all links and add the still existing ones back later
+ linkTableManager.deleteReferenceLink(obj.getId());
+
+ MCRMODSWrapper modsWrapper = new MCRMODSWrapper(obj);
+ final Set categories = new HashSet<>(modsWrapper.getMcrCategoryIDs());
+ if (!categories.isEmpty()) {
+ final MCRCategLinkReference objectReference = new MCRCategLinkReference(obj.getId());
+ MCRCategLinkServiceFactory.obtainInstance().setLinks(objectReference, categories);
+ }
+ List linkingNodes = modsWrapper.getLinkedRelatedItems();
+ List linkingPersons = modsWrapper.getElements(LINKED_PERSONS);
+ List joinedNodes = Stream.concat(linkingNodes.stream(), linkingPersons.stream()).toList();
+ if (!joinedNodes.isEmpty()) {
+ for (Element linkingNode : joinedNodes) {
+ String targetID = linkingNode.getAttributeValue(MCRXlink.HREF, MCRConstants.XLINK_NAMESPACE);
+ if (targetID == null) {
+ continue;
+ }
+ String relationshipTypeRaw = linkingNode.getAttributeValue("type");
+ UBOMODSRelationshipType relType = UBOMODSRelationshipType.fromValue(relationshipTypeRaw);
+ //MCR-1328 (no reference links for 'host')
+ if (relType != UBOMODSRelationshipType.HOST) {
+ linkTableManager.addReferenceLink(obj.getId(), MCRObjectID.getInstance(targetID),
+ MCRLinkTableManager.ENTRY_TYPE_REFERENCE, relType.getValue());
+ }
+ }
+ }
+ }
+
+ /* (non-Javadoc)
+ * @see org.mycore.common.events.MCREventHandlerBase
+ * #handleObjectUpdated(org.mycore.common.events.MCREvent, org.mycore.datamodel.metadata.MCRObject)
+ */
+ @Override
+ protected void handleObjectUpdated(final MCREvent evt, final MCRObject obj) {
+ handleObjectCreated(evt, obj);
+ }
+
+ /* (non-Javadoc)
+ * @see org.mycore.common.events.MCREventHandlerBase
+ * #handleObjectRepaired(org.mycore.common.events.MCREvent, org.mycore.datamodel.metadata.MCRObject)
+ */
+ @Override
+ protected void handleObjectRepaired(final MCREvent evt, final MCRObject obj) {
+ handleObjectUpdated(evt, obj);
+ }
+
+}
diff --git a/ubo-modsperson/src/main/java/org/mycore/ubo/modsperson/linking/UBOMODSRelationshipType.java b/ubo-modsperson/src/main/java/org/mycore/ubo/modsperson/linking/UBOMODSRelationshipType.java
new file mode 100644
index 000000000..189e09745
--- /dev/null
+++ b/ubo-modsperson/src/main/java/org/mycore/ubo/modsperson/linking/UBOMODSRelationshipType.java
@@ -0,0 +1,60 @@
+/*
+ * This file is part of *** M y C o R e ***
+ * See https://www.mycore.de/ for details.
+ *
+ * MyCoRe 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 3 of the License, or
+ * (at your option) any later version.
+ *
+ * MyCoRe 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 MyCoRe. If not, see .
+ */
+package org.mycore.ubo.modsperson.linking;
+
+import java.util.EnumSet;
+import java.util.Map;
+import java.util.function.Function;
+import java.util.stream.Collectors;
+
+/**
+ * Represents all supported relatedItem type supported for metadata sharing and linking.
+ * Overwrites MCRMODSRelationshipType from mycore-base.
+ */
+public enum UBOMODSRelationshipType {
+ HOST("host"), PRECEDING("preceding"), ORIGINAL("original"), SERIES("series"), OTHER_VERSION("otherVersion"),
+ OTHER_FORMAT("otherFormat"), REFERENCES("references"), REVIEW_OF("reviewOf"), PERSONAL("personal");
+
+ private final String value;
+
+ private static final Map RELATIONSHIP_TYPES = EnumSet
+ .allOf(UBOMODSRelationshipType.class)
+ .stream()
+ .collect(Collectors.toMap(UBOMODSRelationshipType::getValue, Function.identity()));
+
+ UBOMODSRelationshipType(String value) {
+ this.value = value;
+ }
+
+ public String getValue() {
+ return value;
+ }
+
+ @Override
+ public String toString() {
+ return value;
+ }
+
+ public static UBOMODSRelationshipType fromValue(String value) {
+ UBOMODSRelationshipType type = RELATIONSHIP_TYPES.get(value);
+ if (type != null) {
+ return type;
+ }
+ throw new IllegalArgumentException("Unknown relatedItem type: " + value);
+ }
+}
diff --git a/ubo-modsperson/src/main/java/org/mycore/ubo/modsperson/merger/MCRNameMerger.java b/ubo-modsperson/src/main/java/org/mycore/ubo/modsperson/merger/MCRNameMerger.java
new file mode 100644
index 000000000..5329c0ef8
--- /dev/null
+++ b/ubo-modsperson/src/main/java/org/mycore/ubo/modsperson/merger/MCRNameMerger.java
@@ -0,0 +1,307 @@
+package org.mycore.ubo.modsperson.merger;
+
+import java.text.Normalizer;
+import java.text.Normalizer.Form;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Locale;
+import java.util.Objects;
+import java.util.Map;
+import java.util.Set;
+
+import org.jdom2.Element;
+import org.mycore.common.MCRConstants;
+import org.mycore.mods.merger.MCRHyphenNormalizer;
+import org.mycore.mods.merger.MCRMerger;
+import org.mycore.mods.merger.MCRMergerFactory;
+
+/**
+ * Compares and merges mods:name elements
+ *
+ * @author Frank Lützenkirchen
+ */
+public class MCRNameMerger extends MCRMerger {
+
+ private String familyName;
+
+ private final Set givenNames = new HashSet<>();
+
+ private final Set initials = new HashSet<>();
+
+ private final Set allNames = new HashSet<>();
+
+ private final Map> nameIds = new HashMap<>();
+
+ public void setElement(Element element) {
+ super.setElement(element);
+
+ setFromNameParts(element);
+
+ if (familyName == null) {
+ setFromDisplayForm(element);
+ }
+
+ collectNameIds(element);
+ }
+
+ private void setFromDisplayForm(Element element) {
+ String displayForm = element.getChildTextTrim("displayForm", MCRConstants.MODS_NAMESPACE);
+ if (displayForm != null) {
+ setFromCombinedName(displayForm.replaceAll("\\s+", " "));
+ }
+ }
+
+ private void setFromNameParts(Element modsName) {
+ for (Element namePart : modsName.getChildren("namePart", MCRConstants.MODS_NAMESPACE)) {
+ String type = namePart.getAttributeValue("type");
+ String nameFragment = namePart.getText().replaceAll("\\p{Zs}+", " ");
+
+ if (Objects.equals(type, "family")) {
+ setFamilyName(nameFragment);
+ } else if (Objects.equals(type, "given")) {
+ addGivenNames(nameFragment);
+ addInitials(nameFragment);
+ allNames.addAll(initials);
+ } else if (Objects.equals(type, "date")) {
+ continue;
+ } else if (Objects.equals(type, "termsOfAddress")) {
+ continue;
+ } else if ("personal".equals(modsName.getAttributeValue("type"))) {
+ setFromCombinedName(nameFragment);
+ } else {
+ setFamilyName(nameFragment);
+ }
+ }
+ }
+
+ private void setFromCombinedName(String nameFragment) {
+ if (isEmptyOrNull(nameFragment)) {
+ return;
+ }
+ if (nameFragment.contains(",")) {
+ String[] parts = nameFragment.split(",");
+ setFamilyNameAndRest(parts[0].trim(), parts[1].trim());
+ } else if (nameFragment.contains(" ")) {
+ int pos = nameFragment.lastIndexOf(' ');
+ setFamilyNameAndRest(nameFragment.substring(pos), nameFragment.substring(0, pos));
+ } else {
+ setFamilyName(nameFragment);
+ }
+ }
+
+ private void setFamilyNameAndRest(String familyName, String rest) {
+ setFamilyName(familyName);
+ addGivenNames(rest);
+ addInitials(rest);
+ allNames.addAll(initials);
+ }
+
+ private void setFamilyName(String nameFragment) {
+ if (isEmptyOrNull(nameFragment)) {
+ return;
+ }
+ this.familyName = normalize(nameFragment);
+ allNames.add(normalize(nameFragment));
+ }
+
+ private void addGivenNames(String nameFragment) {
+ if (isEmptyOrNull(nameFragment)) {
+ return;
+ }
+ for (String token : nameFragment.split("\\s")) {
+ String normalizedToken = normalize(token);
+ if (normalizedToken.length() > 1 && !isEmptyOrNull(normalizedToken)) {
+ givenNames.add(normalizedToken);
+ allNames.add(normalizedToken);
+ }
+ }
+ }
+
+ private void addInitials(String nameFragment) {
+ if (isEmptyOrNull(nameFragment)) {
+ return;
+ }
+ for (String token : nameFragment.split("\\s")) {
+ String normalizedToken = normalize(token);
+ if (!isEmptyOrNull(normalizedToken)) {
+ initials.add(normalizedToken.substring(0, 1));
+ }
+ }
+ }
+
+ private boolean isEmptyOrNull(String nameFragment) {
+ return nameFragment == null || normalize(nameFragment).isEmpty();
+ }
+
+ private String normalize(String nameFragment) {
+ String text = nameFragment.toLowerCase(Locale.getDefault());
+ text = MCRHyphenNormalizer.normalizeHyphen(text, ' ');
+ // canonical decomposition, then remove accents
+ text = Normalizer.normalize(text, Form.NFD).replaceAll("\\p{M}", "");
+ text = text.replace("ue", "u").replace("oe", "o").replace("ae", "a").replace("ß", "s").replace("ss", "s");
+ text = text.replaceAll("[^a-z0-9]\\s]", ""); //remove all non-alphabetic characters
+ text = text.replaceAll("\\p{Punct}", " ").trim(); // remove all punctuation
+ text = text.replaceAll("\\s+", " "); // normalize whitespace
+ return text.trim();
+ }
+
+ /**
+ * Checks if this merger has an alternativeName-element that is
+ * {@link MCRNameMerger#isProbablySameAs(MCRMerger) probably the same as} the other given merger.
+ * @param other the other merger
+ * @return returns true if the other merger is also a {@link MCRNameMerger} and if this merger has
+ * an alternative name that matches the other
+ */
+ public boolean hasAlternativeNameSameAs(MCRMerger other) {
+ if (!(other instanceof MCRNameMerger)) {
+ return false;
+ }
+ return this.element.getChildren("alternativeName", MCRConstants.MODS_NAMESPACE)
+ .stream()
+ .map(MCRMergerFactory::buildFrom)
+ .anyMatch(altMerger -> altMerger.isProbablySameAs(other));
+ }
+
+ @Override
+ public boolean isProbablySameAs(MCRMerger e) {
+ if (!(e instanceof MCRNameMerger other)) {
+ return false;
+ }
+
+ if (haveContradictingNameIds(this.nameIds, other.nameIds)) {
+ return false;
+ } else if (this.allNames.equals(other.allNames)) {
+ return true;
+ } else if (Objects.equals(familyName, other.familyName)) {
+ if (initials.isEmpty() && other.initials.isEmpty()) {
+ return true; // same family name, no given name, no initals, then assumed same
+ } else if (!haveAtLeastOneCommon(this.initials, other.initials)) {
+ return false;
+ } else if (this.givenNames.isEmpty() || other.givenNames.isEmpty()) {
+ return true;
+ } else {
+ return haveAtLeastOneCommon(this.givenNames, other.givenNames);
+ }
+ } else
+ // compound name with same given names assumes same
+ return this.givenNames.equals(other.givenNames) &&
+ (this.familyName.contains(other.familyName) || other.familyName.contains(this.familyName));
+ }
+
+ private boolean haveAtLeastOneCommon(Set a, Set b) {
+ Set intersection = new HashSet<>(a);
+ intersection.retainAll(b);
+ return !intersection.isEmpty();
+ }
+
+ private boolean haveContradictingNameIds(Map> a, Map> b) {
+ Set intersection = null;
+ boolean foundContradictingNameIds = false;
+ for (String type : a.keySet()) {
+ intersection = new HashSet<>(a.get(type));
+ if (b.get(type) != null) {
+ intersection.retainAll(b.get(type));
+ if (!intersection.isEmpty()) {
+ return false;
+ } else {
+ foundContradictingNameIds = true;
+ }
+ }
+ }
+ return foundContradictingNameIds;
+ }
+
+ private void collectNameIds(Element modsName) {
+ for (Element nameId : modsName.getChildren("nameIdentifier", MCRConstants.MODS_NAMESPACE)) {
+ String type = nameId.getAttributeValue("type");
+ String id = nameId.getText();
+
+ Set ids = null;
+ if (nameIds.containsKey(type)) {
+ ids = nameIds.get(type);
+ } else {
+ ids = new HashSet();
+ }
+ ids.add(id);
+ nameIds.put(type, ids);
+ }
+ }
+
+ public String toString() {
+ StringBuilder sb = new StringBuilder(familyName);
+
+ sb.append('|');
+ for (String givenName : givenNames) {
+ sb.append(givenName).append(',');
+ }
+ if (!givenNames.isEmpty()) {
+ sb.setLength(sb.length() - 1);
+ }
+
+ sb.append('|');
+ for (String initial : initials) {
+ sb.append(initial).append(',');
+ }
+ if (!initials.isEmpty()) {
+ sb.setLength(sb.length() - 1);
+ }
+
+ return sb.toString();
+ }
+
+ /**
+ * Merges the contents of the element wrapped by the other merger into a new alternativeName element
+ * in the element wrapped by this merger. Should only be called if this.isProbablySameAs(other).
+ * The alternative name is only added if the two names are not exactly the same and if the
+ * alternative name doesn't yet exist in the element wrapped by this merger.
+ * Only the family name and given names are merged into the alternativeName element.
+ * @param e the other merger
+ */
+ public void mergeAsAlternativeName(MCRMerger e) {
+ if (!(e instanceof MCRNameMerger other)) {
+ return;
+ }
+ if (this.allNames.equals(other.allNames)) {
+ return;
+ }
+ if (this.hasAlternativeNameSameAs(e)) {
+ return;
+ }
+ Element alternativeName = new Element("alternativeName", MCRConstants.MODS_NAMESPACE);
+
+ other.element.getChildren("namePart", MCRConstants.MODS_NAMESPACE)
+ .stream()
+ .filter(namePart -> "given".equals(namePart.getAttributeValue("type")))
+ .forEach(namePart -> {
+ Element altGivenName = new Element("namePart", MCRConstants.MODS_NAMESPACE)
+ .setAttribute("type", "given");
+ altGivenName.addContent(namePart.getText());
+ alternativeName.addContent(altGivenName);
+ });
+
+ Element altFamilyName = new Element("namePart", MCRConstants.MODS_NAMESPACE)
+ .setAttribute("type", "family");
+ Element familyName = other.element.getChildren("namePart", MCRConstants.MODS_NAMESPACE).stream()
+ .filter(namePart -> "family".equals(namePart.getAttributeValue("type")))
+ .findFirst()
+ .orElse(null);
+ altFamilyName.addContent(familyName != null ? familyName.getText() : null);
+ alternativeName.addContent(altFamilyName);
+
+ this.element.addContent(alternativeName);
+
+ }
+
+ @Override
+ public void mergeFrom(MCRMerger other) {
+ super.mergeFrom(other);
+ // if there is family name after merge, prefer that and remove untyped name part
+ if (!getNodes("mods:namePart[@type='family']").isEmpty()) {
+ List namePartsWithoutType = getNodes("mods:namePart[not(@type)]");
+ if (!namePartsWithoutType.isEmpty()) {
+ namePartsWithoutType.get(0).detach();
+ }
+ }
+ }
+}
diff --git a/ubo-modsperson/src/main/java/org/mycore/ubo/modsperson/migration/MODSPersonMigrationCommands.java b/ubo-modsperson/src/main/java/org/mycore/ubo/modsperson/migration/MODSPersonMigrationCommands.java
new file mode 100644
index 000000000..25cf59315
--- /dev/null
+++ b/ubo-modsperson/src/main/java/org/mycore/ubo/modsperson/migration/MODSPersonMigrationCommands.java
@@ -0,0 +1,284 @@
+package org.mycore.ubo.modsperson.migration;
+
+import jakarta.persistence.EntityManager;
+import jakarta.persistence.Query;
+import org.apache.logging.log4j.LogManager;
+import org.apache.logging.log4j.Logger;
+import org.jdom2.Document;
+import org.jdom2.Element;
+import org.jdom2.filter.Filters;
+import org.jdom2.xpath.XPathExpression;
+import org.jdom2.xpath.XPathFactory;
+import org.mycore.access.MCRAccessException;
+import org.mycore.backend.jpa.MCREntityManagerProvider;
+import org.mycore.common.MCRConstants;
+import org.mycore.common.MCRException;
+import org.mycore.common.MCRPersistenceException;
+import org.mycore.common.content.MCRJDOMContent;
+import org.mycore.datamodel.metadata.MCRMetadataManager;
+import org.mycore.datamodel.metadata.MCRObject;
+import org.mycore.datamodel.metadata.MCRObjectID;
+import org.mycore.frontend.cli.MCRCommandUtils;
+import org.mycore.frontend.cli.annotation.MCRCommand;
+import org.mycore.frontend.cli.annotation.MCRCommandGroup;
+import org.mycore.mods.MCRMODSWrapper;
+import org.mycore.orcid2.user.MCRORCIDUser;
+import org.mycore.ubo.modsperson.MODSPersonLookup;
+import org.mycore.ubo.modsperson.MODSPersonUtils;
+import org.mycore.user2.MCRUser;
+import org.mycore.user2.MCRUserManager;
+
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.List;
+
+@MCRCommandGroup(name = "UBO modsperson migration commands")
+public class MODSPersonMigrationCommands {
+
+ private static final Logger LOGGER = LogManager.getLogger();
+ protected static final String CONNECTION_ATTRIBUTE_NAME = "id_connection";
+ protected static final String MODSPERSON_ATTRIBUTE_NAME = "id_modsperson";
+ protected static final String ORCID_AUTH_ATTRIBUTE_PREFIX = MCRORCIDUser.ATTR_ORCID_CREDENTIAL;
+ protected static final String ORCID_USER_PROPERTIES_ATTRIBUTE_PREFIX = MCRORCIDUser.ATTR_ORCID_USER_PROPERTIES;
+
+ /**
+ * Migrates all {@link MCRUser users} with connection IDs to modsperson objects.
+ * Doesn't check for duplicates or existent modsperson objects.
+ */
+ @MCRCommand(
+ syntax = "modsperson migration migrate users to modsperson",
+ help = "FIRST migration command. Creates modsperson objects for all database users. Command is part of a migration process and should "
+ + "only be used once. Should only be used if no modsperson objects exist yet.",
+ order = 1)
+ public static List migrateAllModsperson() {
+ List users = MCRUserManager.listUsers(null, null, null, null, CONNECTION_ATTRIBUTE_NAME, 0, Integer.MAX_VALUE);
+ List commands = new ArrayList<>(users.size());
+ for (MCRUser user : users) {
+ commands.add("modsperson migration migrate user " + user.getUserName() + " in realm "
+ + user.getRealmID() + " to modsperson");
+ }
+ return commands;
+ }
+
+ /**
+ * Migrates a single {@link MCRUser} to a modsperson object using username and realm-id as keys.
+ * @param userName the username to search for
+ * @param realmId the realm to search in
+ */
+ @MCRCommand(
+ syntax = "modsperson migration migrate user {0} in realm {1} to modsperson",
+ help = "Creates a modsperson object for the specific database user {0} in realm {1} (user name, realm id). "
+ + "Command is part of a migration process and should only be used if the corresponding modsperson object "
+ + "doesn't exist yet.",
+ order = 3)
+ public static void migrateModsperson(String userName, String realmId) {
+ MCRUser user = null;
+ try {
+ user = MCRUserManager.getUser(userName, realmId);
+ } catch (Exception e) {
+ LOGGER.error("Error finding user {} in database: {}", userName, e.getMessage());
+ }
+ if (user != null) {
+ final MCRObject modsperson = migrateData(user);
+ try {
+ MCRMetadataManager.create(modsperson);
+ MODSPersonLookup.add(modsperson);
+ addModspersonAttribute(user, modsperson.getId());
+ } catch (MCRPersistenceException | MCRAccessException ex) {
+ LOGGER.warn("Creation of modsperson for user {} failed: {}", user.getUserName(), ex.getMessage());
+ }
+ } else {
+ LOGGER.warn("User {} not found in database", userName);
+ }
+ }
+
+ /**
+ * Deletes all artificially created users that work with the connection-id mechanism.
+ * Should be executed after migration using {@link MODSPersonMigrationCommands#migrateAllModsperson()} first.
+ */
+ @MCRCommand(
+ syntax = "modsperson migration delete users from database",
+ help = "SECOND migration command. Deletes all users from database that have no user account. Command is part of a migration process "
+ + "and should only be used once.",
+ order = 2)
+ public static void deleteUsersFromDatabase() {
+ EntityManager em = MCREntityManagerProvider.getCurrentEntityManager();
+ Query query = em.createQuery("SELECT DISTINCT u FROM MCRUser u "
+ + "JOIN u.attributes a "
+ + "WHERE u.realmID = 'local' "
+ + "AND u.lastLogin IS NULL "
+ + "AND a.name = :connection_ID");
+ query.setParameter("connection_ID", CONNECTION_ATTRIBUTE_NAME);
+ List users;
+ try {
+ users = query.getResultList();
+ } catch (Exception e) {
+ LOGGER.error(e);
+ throw new MCRPersistenceException("Deletion of users failed: ", e);
+ }
+ int count = 0;
+ LOGGER.info("Will delete {} users from database...", users.size());
+ for (MCRUser user : users) {
+ MCRUserManager.deleteUser(user);
+ count++;
+ if (count != 0 && count % 1000 == 0) {
+ LOGGER.info("Deleted {} users from database...", count);
+ }
+ }
+ LOGGER.info("Successfully deleted {} users from database", count);
+ }
+
+ /**
+ * Deletes connection IDs from {@link org.mycore.user2.MCRUserAttribute} and from mods-type
+ * files in the file system.
+ * Should be executed after deletion of artificial users using
+ * {@link MODSPersonMigrationCommands#deleteUsersFromDatabase()}, as artificial user's connection IDs
+ * are already deleted then.
+ */
+ @MCRCommand(
+ syntax = "modsperson migration delete connection ids",
+ help = "THIRD migration command. Deletes all connection ids from database and from publications. "
+ + "Command is part of a migration process and should only be used once. Should be executed after "
+ + "the deletion of artificial users for performance reasons.",
+ order = 4)
+ public static void deleteConnectionIds() throws IOException, MCRAccessException {
+ EntityManager em = MCREntityManagerProvider.getCurrentEntityManager();
+ Query query = em.createQuery("SELECT DISTINCT u FROM MCRUser u "
+ + "JOIN u.attributes a "
+ + "WHERE a.name = :connection_ID");
+ query.setParameter("connection_ID", CONNECTION_ATTRIBUTE_NAME);
+
+ List users = query.getResultList();
+
+ for (MCRUser user : users) {
+ user.getAttributes().removeIf(attr -> CONNECTION_ATTRIBUTE_NAME.equals(attr.getName()));
+ }
+
+ LOGGER.info("Successfully deleted {} connection IDs from database", users.size());
+
+ List ids = MCRCommandUtils.getIdsForType("mods").toList();
+ int count = 0;
+ LOGGER.info("Will search for connection ids in {} publications...", ids.size());
+
+ for (String id : ids) {
+ MCRObject obj = MCRMetadataManager.retrieveMCRObject(MCRObjectID.getInstance(id));
+ Document xmlOld = obj.createXML();
+ Document xmlNew = xmlOld.clone();
+
+ String path = "/mycoreobject/metadata/def.modsContainer/modsContainer/mods:mods/"
+ + "mods:name[@type='personal']/mods:nameIdentifier[@type='connection']";
+
+ XPathExpression xPath =
+ XPathFactory.instance().compile(path, Filters.element(), null, MCRConstants.MODS_NAMESPACE);
+
+ for (Element nameIdentifier : xPath.evaluate(xmlNew)) {
+ nameIdentifier.detach();
+ }
+
+ String oldData = new MCRJDOMContent(xmlOld).asString();
+ String newData = new MCRJDOMContent(xmlNew).asString();
+ if (!oldData.equals(newData)) {
+ MCRObject objNew = new MCRObject(xmlNew);
+ try {
+ MCRMetadataManager.update(objNew);
+ count++;
+ } catch (Exception e) {
+ LOGGER.error("Error while processing object {}, "
+ + "skipping deletion of connection-ID, please process manually: {}", id, e);
+ }
+ }
+ if (count != 0 && count % 1000 == 0) {
+ LOGGER.info("Deleted connection ids from {} publications...", count);
+ }
+ }
+ LOGGER.info("Successfully deleted connection ids from {} publications", count);
+ }
+
+ /**
+ * TODO: Use MCRUserCommands.setUserAttribute once mycore-user2 in 2024.06.x is used.
+ * Adds a new attribute {@link MODSPersonMigrationCommands#MODSPERSON_ATTRIBUTE_NAME} to the database user,
+ * if user is not artificial.
+ * @param user the database user
+ * @param modspersonId the ID to be added as a user attribute
+ */
+ private static void addModspersonAttribute(MCRUser user, MCRObjectID modspersonId) {
+ if (!isArtificialUser(user)) {
+ try {
+ user.setUserAttribute(MODSPERSON_ATTRIBUTE_NAME, modspersonId.toString());
+ MCRUserManager.updateUser(user);
+ } catch (Exception e) {
+ throw new MCRException(
+ "Error while setting attribute " + MODSPERSON_ATTRIBUTE_NAME + " to " + modspersonId +
+ " for user " + user.getUserID() + ": ", e);
+ }
+ }
+ }
+
+ /**
+ * Creates a modsperson-object using the data of a {@link MCRUser}-entity.
+ * @param user the user that contains the data that's migrated to a modsperson.
+ */
+ private static MCRObject migrateData(MCRUser user) {
+ MCRObject modsperson = new MCRObject(new Document(MODSPersonUtils.getMODSPersonTemplate().clone()));
+ MCRMODSWrapper wrapper = new MCRMODSWrapper(modsperson);
+ Element personName = wrapper.getElement("mods:name[@type='personal']");
+
+ String[] nameParts = user.getRealName().split(",");
+ String familyName = nameParts.length < 2 ? user.getRealName() : nameParts[0];
+ String givenName = nameParts.length < 2 ? user.getRealName() : nameParts[1];
+
+ Element familyNamePart = new Element("namePart", MCRConstants.MODS_NAMESPACE)
+ .setAttribute("type", "family")
+ .setText(familyName);
+
+ Element givenNamePart = new Element("namePart", MCRConstants.MODS_NAMESPACE)
+ .setAttribute("type", "given")
+ .setText(givenName);
+
+ personName.addContent(familyNamePart);
+ personName.addContent(givenNamePart);
+
+ user.getAttributes().iterator().forEachRemaining(attribute -> {
+ if (!isExcludedAttribute(attribute.getName())) {
+ final String typeName = attribute.getName().startsWith("id_") ? attribute.getName().substring(3)
+ : attribute.getName();
+ Element nameIdentifier = new Element("nameIdentifier", MCRConstants.MODS_NAMESPACE)
+ .setAttribute("type", typeName)
+ .setText(attribute.getValue());
+ personName.addContent(nameIdentifier);
+ }
+ });
+ return modsperson;
+ }
+
+ /**
+ * Tests if a user was artificially added by the three criteria:
+ *
+ * - has a connection id
+ * - local realm
+ * - has not logged in yet
+ *
+ * @param user user to test
+ * @return true, if user was artificially added
+ */
+ private static boolean isArtificialUser(MCRUser user) {
+ return (user.getAttributes().stream()
+ .anyMatch(attr -> "id_connection".equals(attr.getName())))
+ && user.getRealmID().equals("local") && user.getLastLogin() == null;
+ }
+
+ /**
+ * True, if the attribute name pertains the connection id or the ORCID authentication,
+ * or describes the modsperson itself.
+ * Attribute name "id_orcid" will be migrated.
+ * @param attributeName the name of the tested attribute
+ * @return true, if excluded
+ */
+ private static boolean isExcludedAttribute(String attributeName) {
+ return attributeName.equals(CONNECTION_ATTRIBUTE_NAME)
+ || attributeName.startsWith(MODSPERSON_ATTRIBUTE_NAME)
+ || attributeName.startsWith(ORCID_AUTH_ATTRIBUTE_PREFIX)
+ || attributeName.startsWith(ORCID_USER_PROPERTIES_ATTRIBUTE_PREFIX);
+ }
+
+}
diff --git a/ubo-modsperson/src/main/resources/META-INF/resources/edit-person.xed b/ubo-modsperson/src/main/resources/META-INF/resources/edit-person.xed
new file mode 100644
index 000000000..3fd8419ca
--- /dev/null
+++ b/ubo-modsperson/src/main/resources/META-INF/resources/edit-person.xed
@@ -0,0 +1,150 @@
+
+
+
+ Person erfassen
+ Register person
+
+
+
+
+ Die mit * gekennzeichneten Felder sind Pflichtfelder.
+ Durch klicken auf [+] können Sie ein Eingabefeld wiederholen!
+
+
+
+
+
+
+ Fields marked with * are required fields.
+ By clicking on [+] you can repeat an input field!
+
+
+
+
+
+
+
+
diff --git a/ubo-modsperson/src/main/resources/config/ubo-modsperson/mycore.properties b/ubo-modsperson/src/main/resources/config/ubo-modsperson/mycore.properties
new file mode 100644
index 000000000..7c9104533
--- /dev/null
+++ b/ubo-modsperson/src/main/resources/config/ubo-modsperson/mycore.properties
@@ -0,0 +1,39 @@
+# Register the metadata type "modsperson"
+MCR.Metadata.Type.modsperson=true
+
+# Event handler to collect and link person data in separate modsperson objects
+MCR.EventHandler.MCRObject.022.Class=org.mycore.ubo.modsperson.MODSPersonLinkingEventHandler
+
+# Event handler to process changes in modsperson objects
+MCR.EventHandler.MCRObject.023.Class=org.mycore.ubo.modsperson.MODSPersonEventHandler
+
+# Overwritten EventHandler to properly handle modsperson references
+MCR.EventHandler.MCRObject.040.Class=org.mycore.ubo.modsperson.linking.UBOMODSLinksEventHandler
+
+# Event handler to transfer authenticated ORCID user's ORCIDs into the corresponding modsperson
+MCR.EventHandler.MCRUser.021.Class=org.mycore.ubo.modsperson.linking.MODSPersonORCIDLinkingEventHandler
+
+# Startup handler to initially fill the cache of all known person data
+MCR.Startup.Class=%MCR.Startup.Class%, org.mycore.ubo.modsperson.MODSPersonCachingStartupHandler
+
+# Extend SOLR indexing to also index modsperson objects with custom fields
+MCR.URIResolver.xslImports.solr-document=%MCR.URIResolver.xslImports.solr-document%,modsperson-solr.xsl
+
+# Extend display of metadata to modsperson object
+MCR.URIResolver.xslIncludes.mycoreobject-extensions=mycoreobject-modsperson.xsl
+
+# Override MCRNameMerger from mycore-mods
+MCR.MODS.Merger.name=org.mycore.ubo.modsperson.merger.MCRNameMerger
+MCR.MODS.Merger.alternativeName=org.mycore.ubo.modsperson.merger.MCRNameMerger
+
+MCR.ContentTransformer.modsperson-json-normal.Class=org.mycore.common.content.transformer.MCRXsonifyTransformer
+MCR.ContentTransformer.modsperson-json-normal.Schema=datamodel-mods.xsd
+MCR.ContentTransformer.modsperson-json-normal.Settings.Class=org.mycore.common.content.transformer.MCRXsonifyTransformer$SettingsBuilder
+MCR.ContentTransformer.modsperson-json-normal.Style.Class=org.mycore.common.content.transformer.MCRXsonifyTransformer$StyleBuilder
+MCR.ContentTransformer.modsperson-json-normal.Style.TextKey=text
+
+MCR.ContentTransformer.mycoreobject-modsperson-structure.Stylesheet=xsl/mycoreobject-modsperson-structure.xsl,xsl/html-layout.xsl,xsl/i18n-transformation-step.xsl,xsl/xhtml2html.xsl
+MCR.ContentTransformer.mycoreobject-modsperson-structure.TransformerFactoryClass=org.apache.xalan.processor.TransformerFactoryImpl
+
+# MyCoRe command line interface configuration
+MCR.CLI.Classes.Internal=%MCR.CLI.Classes.Internal%,org.mycore.ubo.modsperson.migration.MODSPersonMigrationCommands
diff --git a/ubo-modsperson/src/main/resources/xsl/modsperson-solr.xsl b/ubo-modsperson/src/main/resources/xsl/modsperson-solr.xsl
new file mode 100644
index 000000000..c23581260
--- /dev/null
+++ b/ubo-modsperson/src/main/resources/xsl/modsperson-solr.xsl
@@ -0,0 +1,56 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ ,
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ ,
+
+
+
+
+
+
+
+
diff --git a/ubo-modsperson/src/main/resources/xsl/mycoreobject-modsperson-structure.xsl b/ubo-modsperson/src/main/resources/xsl/mycoreobject-modsperson-structure.xsl
new file mode 100644
index 000000000..cb6d14305
--- /dev/null
+++ b/ubo-modsperson/src/main/resources/xsl/mycoreobject-modsperson-structure.xsl
@@ -0,0 +1,348 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ person
+
+
+
+ base
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ duplicate
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ col-11 ubo-structure__content
+
+ ubo-structure__content--info
+ ubo-structure__content--warning
+ ubo-structure__content--default
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Evtl. Dublette
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ divs
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Details
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/ubo-modsperson/src/main/resources/xsl/mycoreobject-modsperson.xsl b/ubo-modsperson/src/main/resources/xsl/mycoreobject-modsperson.xsl
new file mode 100644
index 000000000..deb788fce
--- /dev/null
+++ b/ubo-modsperson/src/main/resources/xsl/mycoreobject-modsperson.xsl
@@ -0,0 +1,244 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Person Nr.
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/ubo-modsperson/src/main/resources/xsl/response-person.xsl b/ubo-modsperson/src/main/resources/xsl/response-person.xsl
new file mode 100644
index 000000000..b9d59230a
--- /dev/null
+++ b/ubo-modsperson/src/main/resources/xsl/response-person.xsl
@@ -0,0 +1,151 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+ :
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ | Name |
+ LSF-ID |
+ Scopus-ID |
+ Orcid-ID |
+ GND-ID |
+ Researcher-ID |
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ |
+
+
+
+ ,
+
+
+ |
+
+
+
+ |
+
+
+
+ |
+
+
+
+ |
+
+
+
+ |
+
+
+
+ |
+
+
+
+
+
+
+
diff --git a/ubo-modsperson/src/test/java/org/mycore/ubo/modsperson/MODSPersonEventHandlerTest.java b/ubo-modsperson/src/test/java/org/mycore/ubo/modsperson/MODSPersonEventHandlerTest.java
new file mode 100644
index 000000000..8defea622
--- /dev/null
+++ b/ubo-modsperson/src/test/java/org/mycore/ubo/modsperson/MODSPersonEventHandlerTest.java
@@ -0,0 +1,122 @@
+package org.mycore.ubo.modsperson;
+
+import org.jdom2.Document;
+import org.jdom2.Element;
+import org.jdom2.JDOMException;
+import org.junit.Test;
+import org.mycore.access.MCRAccessException;
+import org.mycore.common.MCRConstants;
+import org.mycore.common.MCRStoreTestCase;
+import org.mycore.common.content.MCRURLContent;
+import org.mycore.datamodel.common.MCRActiveLinkException;
+import org.mycore.datamodel.common.MCRLinkTableManager;
+import org.mycore.datamodel.metadata.MCRMetadataManager;
+import org.mycore.datamodel.metadata.MCRObject;
+import org.mycore.datamodel.metadata.MCRObjectID;
+import org.mycore.datamodel.metadata.MCRObjectMetadataTest;
+import org.mycore.mods.MCRMODSWrapper;
+import org.mycore.user2.MCRUser;
+import org.mycore.user2.MCRUserManager;
+
+import java.io.IOException;
+import java.net.URL;
+import java.util.List;
+import java.util.Set;
+
+import static org.junit.Assert.*;
+
+public class MODSPersonEventHandlerTest extends MCRStoreTestCase {
+
+ @Test
+ public void testHandleObjectUpdated() throws IOException, JDOMException, MCRAccessException {
+ URL url1 = MCRObjectMetadataTest.class.getResource(
+ "/MODSPersonEventHandlerTest/junit_modsperson_00000016.xml");
+ Document doc1 = new MCRURLContent(url1).asXML();
+ MCRObject person1 = new MCRObject(doc1);
+
+ MCRMetadataManager.create(person1);
+
+ MCRMODSWrapper wrapper = new MCRMODSWrapper(person1);
+ Element modsName = wrapper.getMODS().getChild("name", MCRConstants.MODS_NAMESPACE);
+ Element familyName = modsName.getChildren("namePart", MCRConstants.MODS_NAMESPACE)
+ .stream()
+ .filter(el -> "family".equals(el.getAttributeValue("type"))).findFirst().get();
+ familyName.setText("Meyer");
+
+ Set foundPersons = MODSPersonLookup.lookup(modsName);
+ assertNull(foundPersons);
+
+ MCRMetadataManager.update(person1);
+
+ foundPersons = MODSPersonLookup.lookup(modsName);
+ assertNotNull(foundPersons);
+ assertEquals(1, foundPersons.size());
+ assertEquals("Meyer", foundPersons.iterator().next().getFamilyName());
+ assertEquals("Peter", foundPersons.iterator().next().getGivenName());
+ assertEquals("99999|lsf", foundPersons.iterator().next().getKeys().iterator().next());
+
+ familyName.setText("Müller");
+ foundPersons = MODSPersonLookup.lookup(modsName);
+ assertNull(foundPersons);
+ }
+
+ @Test
+ public void testHandleObjectDeleted() throws IOException, JDOMException, MCRAccessException,
+ MCRActiveLinkException {
+ MCRLinkTableManager linkTableManager = MCRLinkTableManager.getInstance();
+
+ URL url1 = MCRObjectMetadataTest.class.getResource(
+ "/MODSPersonEventHandlerTest/junit_modsperson_00000016.xml");
+ Document doc1 = new MCRURLContent(url1).asXML();
+ MCRObject person1 = new MCRObject(doc1);
+
+ URL url2 = MCRObjectMetadataTest.class.getResource(
+ "/MODSPersonEventHandlerTest/junit_mods_00000017.xml");
+ Document doc2 = new MCRURLContent(url2).asXML();
+ MCRObject obj1 = new MCRObject(doc2);
+
+ MCRMODSWrapper wrapper = new MCRMODSWrapper(obj1);
+ Element modsName = wrapper.getMODS().getChild("name", MCRConstants.MODS_NAMESPACE);
+ assertNotNull(modsName.getAttribute("href", MCRConstants.XLINK_NAMESPACE));
+
+ MCRMetadataManager.create(person1);
+ MCRMetadataManager.create(obj1);
+
+ assertEquals(1, linkTableManager.countReferenceLinkTo("junit_modsperson_00000016"));
+ List destinations = (List) linkTableManager
+ .getDestinationOf("junit_mods_00000017", "reference");
+ assertEquals(1, destinations.size());
+ assertEquals("junit_modsperson_00000016", destinations.getFirst());
+
+ assertThrows(MCRActiveLinkException.class, () -> MCRMetadataManager.delete(person1));
+
+ linkTableManager.deleteReferenceLink(obj1.getId());
+ MCRMetadataManager.delete(person1);
+
+ assertEquals(0, linkTableManager.countReferenceLinkTo("junit_modsperson_00000016"));
+ destinations = (List) linkTableManager
+ .getDestinationOf("junit_mods_00000017", "reference");
+ assertEquals(0, destinations.size());
+
+ obj1 = MCRMetadataManager.retrieveMCRObject(MCRObjectID.getInstance("junit_mods_00000017"));
+ assertNotNull(obj1);
+ }
+
+ @Test
+ public void testHandleObjectDeletedUser()
+ throws IOException, JDOMException, MCRAccessException, MCRActiveLinkException {
+ URL url1 = MCRObjectMetadataTest.class.getResource(
+ "/MODSPersonEventHandlerTest/junit_modsperson_00000018.xml");
+ Document doc1 = new MCRURLContent(url1).asXML();
+ MCRObject person1 = new MCRObject(doc1);
+ MCRMetadataManager.create(person1);
+
+ MCRUser mcrUser = new MCRUser("userName", "local");
+ mcrUser.setUserAttribute("id_modsperson", "junit_modsperson_00000018");
+ MCRUserManager.createUser(mcrUser);
+ MCRMetadataManager.delete(person1);
+
+ mcrUser = MCRUserManager.getUser("userName", "local");
+ assertEquals(0, mcrUser.getAttributes().size());
+ }
+}
diff --git a/ubo-modsperson/src/test/java/org/mycore/ubo/modsperson/MODSPersonLinkingEventHandlerTest.java b/ubo-modsperson/src/test/java/org/mycore/ubo/modsperson/MODSPersonLinkingEventHandlerTest.java
new file mode 100644
index 000000000..6463afb53
--- /dev/null
+++ b/ubo-modsperson/src/test/java/org/mycore/ubo/modsperson/MODSPersonLinkingEventHandlerTest.java
@@ -0,0 +1,259 @@
+package org.mycore.ubo.modsperson;
+
+import org.jdom2.Document;
+import org.jdom2.Element;
+import org.jdom2.JDOMException;
+import org.jdom2.output.Format;
+import org.jdom2.output.XMLOutputter;
+import org.junit.Before;
+import org.junit.Test;
+import org.mycore.access.MCRAccessException;
+import org.mycore.common.MCRConstants;
+import org.mycore.common.MCRStoreTestCase;
+import org.mycore.common.content.MCRURLContent;
+import org.mycore.datamodel.metadata.MCRMetadataManager;
+import org.mycore.datamodel.metadata.MCRObject;
+import org.mycore.datamodel.metadata.MCRObjectID;
+import org.mycore.datamodel.metadata.MCRObjectMetadataTest;
+import org.mycore.frontend.cli.MCRCommandUtils;
+import org.mycore.mods.MCRMODSWrapper;
+
+import java.io.IOException;
+import java.net.URL;
+import java.util.List;
+
+import static org.junit.Assert.*;
+
+public class MODSPersonLinkingEventHandlerTest extends MCRStoreTestCase {
+
+ private MODSPersonLinkingEventHandler handler;
+
+ @Before
+ public void setUp() throws Exception {
+ super.setUp();
+ handler = new MODSPersonLinkingEventHandler();
+ }
+
+ /**
+ * Tests that for three similar but not identical person objects, three different modsperson objects are created.
+ * @throws IOException in case of error
+ * @throws JDOMException in case of error
+ * @throws MCRAccessException in case of error
+ */
+ @Test
+ public void testHandleCreate() throws IOException, JDOMException, MCRAccessException {
+ URL url1 = MCRObjectMetadataTest.class.getResource(
+ "/MODSPersonLinkingEventHandlerTest/junit_mods_00000001.xml");
+ Document doc1 = new MCRURLContent(url1).asXML();
+ MCRObject obj1 = new MCRObject(doc1);
+
+ URL url2 = MCRObjectMetadataTest.class.getResource(
+ "/MODSPersonLinkingEventHandlerTest/junit_mods_00000002.xml");
+ Document doc2 = new MCRURLContent(url2).asXML();
+ MCRObject obj2 = new MCRObject(doc2);
+
+ URL url3 = MCRObjectMetadataTest.class.getResource(
+ "/MODSPersonLinkingEventHandlerTest/junit_mods_00000003.xml");
+ Document doc3 = new MCRURLContent(url3).asXML();
+ MCRObject obj3 = new MCRObject(doc3);
+
+ MCRMetadataManager.create(obj1);
+ MCRMetadataManager.create(obj2);
+ MCRMetadataManager.create(obj3);
+
+ handler.handleObjectCreated(null, obj1);
+ handler.handleObjectCreated(null, obj2);
+ handler.handleObjectCreated(null, obj3);
+
+ List modspersons = MCRCommandUtils.getIdsForType("modsperson").toList();
+ assertEquals(3, modspersons.size());
+
+ MCRObject person4 = MCRMetadataManager.retrieveMCRObject(MCRObjectID
+ .getInstance(modspersons.get(0)));
+ assertNotNull(person4);
+ assertPerson(person4, "Müller", "Lisa", "77777", "1112222999");
+
+ MCRObject person5 = MCRMetadataManager.retrieveMCRObject(MCRObjectID
+ .getInstance(modspersons.get(1)));
+ assertNotNull(person5);
+ assertPerson(person5, "Meyer", "Lena", "44444", "1112222888");
+
+ MCRObject person6 = MCRMetadataManager.retrieveMCRObject(MCRObjectID
+ .getInstance(modspersons.get(2)));
+ assertNotNull(person6);
+ assertPerson(person6, "Müller", "Luisa", "99999", "444555777");
+ }
+
+ /**
+ * Tests if compound-names are recognized as alternativeNames of modsperson
+ * @throws IOException in case of error
+ * @throws JDOMException in case of error
+ * @throws MCRAccessException in case of error
+ */
+
+ @Test
+ public void testHandleCreateCompoundName() throws IOException, JDOMException, MCRAccessException {
+ URL url1 = MCRObjectMetadataTest.class.getResource(
+ "/MODSPersonLinkingEventHandlerTest/junit_mods_00000007.xml");
+ Document doc1 = new MCRURLContent(url1).asXML();
+ MCRObject obj1 = new MCRObject(doc1);
+
+ URL url2 = MCRObjectMetadataTest.class.getResource(
+ "/MODSPersonLinkingEventHandlerTest/junit_mods_00000008.xml");
+ Document doc2 = new MCRURLContent(url2).asXML();
+ MCRObject obj2 = new MCRObject(doc2);
+
+ MCRMetadataManager.create(obj1);
+ MCRMetadataManager.create(obj2);
+
+ handler.handleObjectCreated(null, obj1);
+ handler.handleObjectCreated(null, obj2);
+
+ List modspersons = MCRCommandUtils.getIdsForType("modsperson").toList();
+ assertEquals(1, modspersons.size());
+
+ MCRObject person1 = MCRMetadataManager.retrieveMCRObject(MCRObjectID
+ .getInstance(modspersons.get(0)));
+ assertNotNull(person1);
+ assertPerson(person1, "Richter", "Hans Peter", "88888", "3332222999");
+
+ List altNameElements = new MCRMODSWrapper(person1).getMODS().getChild(
+ "name", MCRConstants.MODS_NAMESPACE).getChildren("alternativeName", MCRConstants.MODS_NAMESPACE);
+ assertEquals(1, altNameElements.size());
+ assertAlternativeName(person1, "Richter-Doppelname", "Hans Peter");
+
+ Element mods = new MCRMODSWrapper(person1).getMODS();
+ new XMLOutputter(Format.getPrettyFormat()).output(mods, System.out);
+ }
+
+ /**
+ * Tests the case that a lookup returns multiple person-objects.
+ * @throws IOException in case of error
+ * @throws JDOMException in case of error
+ * @throws MCRAccessException in case of error
+ */
+ @Test
+ public void testHandleCreateDuplicatePerson() throws IOException, JDOMException, MCRAccessException {
+ URL url1 = MCRObjectMetadataTest.class.getResource(
+ "/MODSPersonLinkingEventHandlerTest/junit_mods_00000004.xml");
+ Document doc1 = new MCRURLContent(url1).asXML();
+ MCRObject obj1 = new MCRObject(doc1);
+
+ URL url2 = MCRObjectMetadataTest.class.getResource(
+ "/MODSPersonLinkingEventHandlerTest/junit_mods_00000005.xml");
+ Document doc2 = new MCRURLContent(url2).asXML();
+ MCRObject obj2 = new MCRObject(doc2);
+
+ URL url3 = MCRObjectMetadataTest.class.getResource(
+ "/MODSPersonLinkingEventHandlerTest/junit_mods_00000006.xml");
+ Document doc3 = new MCRURLContent(url3).asXML();
+ MCRObject obj3 = new MCRObject(doc3);
+
+ MCRMetadataManager.create(obj1);
+ MCRMetadataManager.create(obj2);
+ MCRMetadataManager.create(obj3);
+
+ handler.handleObjectCreated(null, obj1);
+ handler.handleObjectCreated(null, obj2);
+ handler.handleObjectCreated(null, obj3);
+
+ List modspersons = MCRCommandUtils.getIdsForType("modsperson").toList();
+ assertEquals(2, modspersons.size());
+
+ MCRObject modsperson1 = MCRMetadataManager.retrieveMCRObject(MCRObjectID.getInstance(modspersons.get(0)));
+ MCRMODSWrapper wrapper = new MCRMODSWrapper(modsperson1);
+ List lsfs = wrapper.getMODS().getChild("name", MCRConstants.MODS_NAMESPACE)
+ .getChildren("nameIdentifier", MCRConstants.MODS_NAMESPACE)
+ .stream()
+ .filter(ni -> "lsf".equals(ni.getAttributeValue("type")))
+ .map(Element::getText).toList();
+
+ if (lsfs.size() == 2) {
+ assertTrue(lsfs.get(0).equals("77766") && lsfs.get(1).equals("77788") ||
+ lsfs.get(0).equals("77788") && lsfs.get(1).equals("77766"));
+ } else {
+ fail(modsperson1.getId().toString() + " is expected to have two LSF-IDs");
+ }
+
+ MCRObject modsperson2 = MCRMetadataManager.retrieveMCRObject(MCRObjectID.getInstance(modspersons.get(1)));
+ wrapper = new MCRMODSWrapper(modsperson2);
+ lsfs = wrapper.getMODS().getChild("name", MCRConstants.MODS_NAMESPACE)
+ .getChildren("nameIdentifier", MCRConstants.MODS_NAMESPACE)
+ .stream()
+ .filter(ni -> "lsf".equals(ni.getAttributeValue("type")))
+ .map(Element::getText).toList();
+
+ if (lsfs.size() == 1) {
+ assertEquals("10101", lsfs.get(0));
+ } else {
+ fail(modsperson2.getId().toString() + " is expected to have one LSF-ID");
+ }
+ }
+
+ /**
+ * Asserts that the first alternativeName found in the MODS of a person fits the expected given and family name
+ * @param person the {@link MCRObject person} tested for an alternative name
+ * @param familyName the expected family name
+ * @param givenName the expected given name
+ */
+ private void assertAlternativeName(MCRObject person, String familyName, String givenName) {
+ MCRMODSWrapper wrapper = new MCRMODSWrapper(person);
+ List altNameElements = wrapper.getMODS().getChild("name", MCRConstants.MODS_NAMESPACE)
+ .getChild("alternativeName", MCRConstants.MODS_NAMESPACE).getChildren();
+ List familyNameElements = altNameElements.stream()
+ .filter(e -> "family".equals(e.getAttributeValue("type")))
+ .toList();
+ List givenNameElements = altNameElements.stream()
+ .filter(e -> "given".equals(e.getAttributeValue("type")))
+ .toList();
+
+ assertEquals(1, givenNameElements.size());
+ assertEquals(givenName, givenNameElements.get(0).getText());
+
+ assertEquals(1, familyNameElements.size());
+ assertEquals(familyName, familyNameElements.get(0).getText());
+ }
+
+ /**
+ * Assert that the Object contains the two name parts and an LSF and SCOPUS ID
+ * @param person tested {@link MCRObject}
+ * @param familyName the expected family name
+ * @param givenName the expected given name
+ * @param lsfId the expected LSF ID
+ * @param scopusId the expected Scopus ID
+ */
+ private void assertPerson(MCRObject person, String familyName,
+ String givenName, String lsfId, String scopusId) {
+
+ MCRMODSWrapper wrapper = new MCRMODSWrapper(person);
+ List nameElements = wrapper.getMODS().getChild("name", MCRConstants.MODS_NAMESPACE)
+ .getChildren();
+ List familyNameElements = nameElements.stream()
+ .filter(e -> "family".equals(e.getAttributeValue("type")))
+ .toList();
+
+ assertEquals(1, familyNameElements.size());
+ assertEquals(familyName, familyNameElements.get(0).getText());
+
+ List givenNameElements = nameElements.stream()
+ .filter(e -> "given".equals(e.getAttributeValue("type")))
+ .toList();
+
+ assertEquals(1, givenNameElements.size());
+ assertEquals(givenName, givenNameElements.get(0).getText());
+
+ List lsfElements = nameElements.stream()
+ .filter(e -> "lsf".equals(e.getAttributeValue("type")))
+ .toList();
+
+ assertEquals(1, lsfElements.size());
+ assertEquals(lsfId, lsfElements.get(0).getText());
+
+ List scopusElements = nameElements.stream()
+ .filter(e -> "scopus".equals(e.getAttributeValue("type")))
+ .toList();
+
+ assertEquals(1, scopusElements.size());
+ assertEquals(scopusId, scopusElements.get(0).getText());
+ }
+}
diff --git a/ubo-modsperson/src/test/java/org/mycore/ubo/modsperson/MODSPersonLookupTest.java b/ubo-modsperson/src/test/java/org/mycore/ubo/modsperson/MODSPersonLookupTest.java
new file mode 100644
index 000000000..befa993c3
--- /dev/null
+++ b/ubo-modsperson/src/test/java/org/mycore/ubo/modsperson/MODSPersonLookupTest.java
@@ -0,0 +1,159 @@
+package org.mycore.ubo.modsperson;
+
+import org.jdom2.Document;
+import org.jdom2.Element;
+import org.jdom2.JDOMException;
+import org.junit.Test;
+import org.mycore.common.MCRConstants;
+import org.mycore.common.MCRTestCase;
+import org.mycore.common.content.MCRURLContent;
+import org.mycore.datamodel.metadata.MCRObject;
+import org.mycore.datamodel.metadata.MCRObjectMetadataTest;
+import org.mycore.mods.MCRMODSWrapper;
+
+import java.io.IOException;
+import java.net.URL;
+import java.util.List;
+import java.util.Objects;
+import java.util.Set;
+
+import static org.junit.Assert.*;
+
+public class MODSPersonLookupTest extends MCRTestCase {
+
+ /**
+ * Tests that only people added to the lookup can be found through that same lookup. Test that people
+ * that are returned contain all added data.
+ * @throws IOException in case of error
+ * @throws JDOMException in case of error
+ */
+ @Test
+ public void testLookup() throws IOException, JDOMException {
+ MODSPersonLookup.clear();
+ URL url1 = MCRObjectMetadataTest.class.getResource("/MODSPersonLookupTest/junit_modsperson_00000010.xml");
+ Document doc1 = new MCRURLContent(url1).asXML();
+ MCRObject obj1 = new MCRObject(doc1);
+
+ URL url2 = MCRObjectMetadataTest.class.getResource("/MODSPersonLookupTest/junit_modsperson_00000011.xml");
+ Document doc2 = new MCRURLContent(url2).asXML();
+ MCRObject obj2 = new MCRObject(doc2);
+
+ URL url3 = MCRObjectMetadataTest.class.getResource("/MODSPersonLookupTest/junit_modsperson_00000012.xml");
+ Document doc3 = new MCRURLContent(url3).asXML();
+ MCRObject obj3 = new MCRObject(doc3);
+
+ MODSPersonLookup.add(obj1);
+ MODSPersonLookup.add(obj2);
+
+ MCRMODSWrapper wrapper = new MCRMODSWrapper(obj1);
+ Element name1 = wrapper.getElement("mods:name[@type='personal']");
+ MODSPersonLookup.PersonCache person1 = Objects.requireNonNull(MODSPersonLookup.lookup(name1)).iterator().next();
+ assertPerson(person1, "Müller", "Adam", "98765", "2222222333");
+
+ assertEquals(2, person1.getAlternativeNames().size());
+ person1.getAlternativeNames().forEach(name -> {
+ if (name.getKey().equals("Müller")) {
+ assertEquals("A", name.getValue());
+ } else if (name.getKey().equals("Müller Meyer")) {
+ assertEquals("Adam", name.getValue());
+ } else {
+ fail("Unknown alternative name:" + name.getKey() + ", " + name.getValue());
+ }
+ });
+
+ wrapper = new MCRMODSWrapper(obj2);
+ Element name2 = wrapper.getElement("mods:name[@type='personal']");
+ MODSPersonLookup.PersonCache person2 = Objects.requireNonNull(MODSPersonLookup.lookup(name2)).iterator().next();
+ assertPerson(person2, "Meyer", "Gustav", "112233", "2222222444");
+
+ wrapper = new MCRMODSWrapper(obj3);
+ Element name3 = wrapper.getElement("mods:name[@type='personal']");
+ Set persons3 = MODSPersonLookup.lookup(name3);
+ assertNull(persons3);
+ }
+
+ /**
+ * Tests if a widely different alternative name can be used to match a person.
+ * @throws IOException in case of error
+ * @throws JDOMException in case of error
+ */
+ @Test
+ public void testLookupAlternateName() throws IOException, JDOMException {
+ MODSPersonLookup.clear();
+ URL url1 = MCRObjectMetadataTest.class.getResource("/MODSPersonLookupTest/junit_modsperson_00000012.xml");
+ Document doc1 = new MCRURLContent(url1).asXML();
+ MCRObject person1 = new MCRObject(doc1);
+
+ MODSPersonLookup.add(person1);
+
+ URL url2 = MCRObjectMetadataTest.class.getResource("/MODSPersonLookupTest/junit_mods_00000009.xml");
+ Document doc2 = new MCRURLContent(url2).asXML();
+ MCRObject obj1 = new MCRObject(doc2);
+
+ MCRMODSWrapper wrapper = new MCRMODSWrapper(obj1);
+ Element name1 = wrapper.getElement("mods:name[@type='personal']");
+
+ MODSPersonLookup.PersonCache personAlt = Objects.requireNonNull(MODSPersonLookup.lookup(name1)).iterator().next();
+ assertNotNull(personAlt);
+ assertPerson(personAlt, "Müller", "Gustavo", "77777", "555555666");
+ }
+
+
+
+ private void assertPerson(MODSPersonLookup.PersonCache assertPerson, String familyName,
+ String givenName, String lsfId, String scopusId) {
+ assertEquals(familyName, assertPerson.getFamilyName());
+ assertEquals(givenName, assertPerson.getGivenName());
+ assertEquals(2, assertPerson.getKeys().size());
+
+ String lsfIdKey = String.join("|", lsfId, "lsf");
+ String scopusIdKey = String.join("|", scopusId, "scopus");
+
+ assertPerson.getKeys().forEach(key -> {
+ assertTrue(key.equals(lsfIdKey) || key.equals(scopusIdKey));
+ });
+ }
+
+ /**
+ * Assert that the Element contains the two name parts and an LSF and SCOPUS ID
+ * @param person tested Element
+ * @param familyName the expected family name
+ * @param givenName the expected given name
+ * @param lsfId the expected LSF ID
+ * @param scopusId the expected Scopus ID
+ */
+ private void assertPersonElement(Element person, String familyName,
+ String givenName, String lsfId, String scopusId) {
+
+ List nameElements = person.getChild("name", MCRConstants.MODS_NAMESPACE)
+ .getChildren();
+ List familyNameElements = nameElements.stream()
+ .filter(e -> "family".equals(e.getAttributeValue("type")))
+ .toList();
+
+ assertEquals(1, familyNameElements.size());
+ assertEquals(familyName, familyNameElements.get(0).getText());
+
+ List givenNameElements = nameElements.stream()
+ .filter(e -> "given".equals(e.getAttributeValue("type")))
+ .toList();
+
+ assertEquals(1, givenNameElements.size());
+ assertEquals(givenName, givenNameElements.get(0).getText());
+
+ List lsfElements = nameElements.stream()
+ .filter(e -> "lsf".equals(e.getAttributeValue("type")))
+ .toList();
+
+ assertEquals(1, lsfElements.size());
+ assertEquals(lsfId, lsfElements.get(0).getText());
+
+ List scopusElements = nameElements.stream()
+ .filter(e -> "scopus".equals(e.getAttributeValue("type")))
+ .toList();
+
+ assertEquals(1, scopusElements.size());
+ assertEquals(scopusId, scopusElements.get(0).getText());
+ }
+
+}
diff --git a/ubo-modsperson/src/test/java/org/mycore/ubo/modsperson/linking/MODSPersonORCIDLinkingEventHandlerTest.java b/ubo-modsperson/src/test/java/org/mycore/ubo/modsperson/linking/MODSPersonORCIDLinkingEventHandlerTest.java
new file mode 100644
index 000000000..ab47d4a22
--- /dev/null
+++ b/ubo-modsperson/src/test/java/org/mycore/ubo/modsperson/linking/MODSPersonORCIDLinkingEventHandlerTest.java
@@ -0,0 +1,59 @@
+package org.mycore.ubo.modsperson.linking;
+
+import org.jdom2.Document;
+import org.jdom2.Element;
+import org.jdom2.JDOMException;
+import org.junit.Test;
+import org.mycore.access.MCRAccessException;
+import org.mycore.common.MCRStoreTestCase;
+import org.mycore.common.content.MCRURLContent;
+import org.mycore.datamodel.metadata.MCRMetadataManager;
+import org.mycore.datamodel.metadata.MCRObject;
+import org.mycore.datamodel.metadata.MCRObjectID;
+import org.mycore.datamodel.metadata.MCRObjectMetadataTest;
+import org.mycore.mods.MCRMODSWrapper;
+import org.mycore.user2.MCRUser;
+import org.mycore.user2.MCRUserManager;
+
+import java.io.IOException;
+import java.net.URL;
+import java.util.List;
+
+import static org.junit.Assert.*;
+import static org.mycore.common.MCRConstants.MODS_NAMESPACE;
+
+public class MODSPersonORCIDLinkingEventHandlerTest extends MCRStoreTestCase {
+
+ @Test
+ public void testDoHandleEvent() throws IOException, JDOMException, MCRAccessException {
+ URL url = MCRObjectMetadataTest.class.getResource(
+ "/MODSPersonORCIDLinkingEventHandlerTest/junit_modsperson_00000019.xml");
+ Document doc1 = new MCRURLContent(url).asXML();
+ MCRObject person1 = new MCRObject(doc1);
+ MCRMetadataManager.create(person1);
+
+ MCRUser mcrUser = new MCRUser("userName", "local");
+ mcrUser.setUserAttribute("id_modsperson", "junit_modsperson_00000019");
+ mcrUser.setUserAttribute("orcid_credential_0000-0000-0000-0001", "{}");
+ mcrUser.setUserAttribute("orcid_credential_0000-0000-0000-0002", "{}");
+
+ MCRUserManager.createUser(mcrUser);
+
+ MCRObject modsperson = MCRMetadataManager.retrieveMCRObject(MCRObjectID
+ .getInstance("junit_modsperson_00000019"));
+ MCRMODSWrapper wrapper = new MCRMODSWrapper(modsperson);
+ Element name = wrapper.getElement("mods:name[@type='personal']");
+ List nameIdentifiers = name.getChildren("nameIdentifier", MODS_NAMESPACE);
+ assertEquals(3, nameIdentifiers.size());
+ assertTrue(nameIdentifiers.stream().anyMatch(e ->
+ "lsf".equals(e.getAttributeValue("type")) && "123456".equals(e.getText())
+ ));
+ assertTrue(nameIdentifiers.stream().anyMatch(e ->
+ "orcid".equals(e.getAttributeValue("type")) && "0000-0000-0000-0001".equals(e.getText())
+ ));
+ assertTrue(nameIdentifiers.stream().anyMatch(e ->
+ "orcid".equals(e.getAttributeValue("type")) && "0000-0000-0000-0002".equals(e.getText())
+ ));
+
+ }
+}
diff --git a/ubo-modsperson/src/test/java/org/mycore/ubo/modsperson/migration/MODSPersonMigrationCommandsTest.java b/ubo-modsperson/src/test/java/org/mycore/ubo/modsperson/migration/MODSPersonMigrationCommandsTest.java
new file mode 100644
index 000000000..4f219952f
--- /dev/null
+++ b/ubo-modsperson/src/test/java/org/mycore/ubo/modsperson/migration/MODSPersonMigrationCommandsTest.java
@@ -0,0 +1,267 @@
+package org.mycore.ubo.modsperson.migration;
+
+import org.jdom2.Document;
+import org.jdom2.Element;
+import org.jdom2.JDOMException;
+import org.junit.Before;
+import org.junit.Test;
+import org.mycore.access.MCRAccessException;
+import org.mycore.common.MCRConstants;
+import org.mycore.common.MCRStoreTestCase;
+import org.mycore.common.content.MCRURLContent;
+import org.mycore.datamodel.metadata.MCRMetadataManager;
+import org.mycore.datamodel.metadata.MCRObject;
+import org.mycore.datamodel.metadata.MCRObjectID;
+import org.mycore.datamodel.metadata.MCRObjectMetadataTest;
+import org.mycore.frontend.cli.MCRCommandUtils;
+import org.mycore.mods.MCRMODSWrapper;
+import org.mycore.ubo.modsperson.MODSPersonLookup;
+import org.mycore.user2.MCRRealmFactory;
+import org.mycore.user2.MCRUser;
+import org.mycore.user2.MCRUserManager;
+
+import java.io.IOException;
+import java.net.URL;
+import java.time.Instant;
+import java.util.Date;
+import java.util.List;
+import java.util.Set;
+import java.util.UUID;
+
+import static org.junit.Assert.*;
+
+public class MODSPersonMigrationCommandsTest extends MCRStoreTestCase {
+
+ @Before
+ public void setUp() throws Exception {
+ super.setUp();
+ MODSPersonLookup.clear();
+ }
+
+ @Test
+ public void testMigration() {
+ String userName1 = "test1";
+ String userName2 = "test2";
+
+ MCRUser userLocal = new MCRUser(userName1, MCRRealmFactory.getLocalRealm());
+ userLocal.setRealName("Tester, Peter");
+ userLocal.setUserAttribute("id_connection", "123");
+ MCRUser userOther = new MCRUser(userName2, MCRRealmFactory.getRealm("test-realm"));
+ userOther.setRealName("Petra Tester");
+
+ MCRUserManager.createUser(userLocal);
+ MCRUserManager.createUser(userOther);
+
+ MODSPersonMigrationCommands.migrateModsperson(userName1, "local");
+ MODSPersonMigrationCommands.migrateModsperson(userName2, "test-realm");
+
+ userLocal = MCRUserManager.getUser(userName1, "local");
+ assertEquals(1, userLocal.getAttributes().size());
+ assertEquals(MODSPersonMigrationCommands.CONNECTION_ATTRIBUTE_NAME, userLocal.getAttributes().first().getName());
+ assertEquals("123", userLocal.getAttributes().first().getValue());
+
+ userOther = MCRUserManager.getUser(userName2, "test-realm");
+ assertEquals(1, userOther.getAttributes().size());
+ assertEquals(MODSPersonMigrationCommands.MODSPERSON_ATTRIBUTE_NAME, userOther.getAttributes().first().getName());
+ assertTrue(userOther.getAttributes().first().getValue().contains("junit_modsperson_0000"));
+ assertEquals(2, MODSPersonLookup.getCacheSize());
+ }
+
+ @Test
+ public void testMigrationUserNotFound() {
+ MODSPersonMigrationCommands.migrateModsperson("userNotFound", "local");
+ assertEquals(0, MODSPersonLookup.getCacheSize());
+ }
+
+ @Test
+ public void testMigrationUserAttributes() {
+ String userName = "username";
+ MCRUser user = new MCRUser(userName, MCRRealmFactory.getLocalRealm());
+ user.setRealName("Tester, Peter");
+ user.setLastLogin(Date.from(Instant.now()));
+ user.setUserAttribute("id_connection", userName);
+ user.setUserAttribute("id_orcid", "000-0001-2345-6789");
+ user.setUserAttribute("id_scopus", "00011112222");
+ user.setUserAttribute("orcid_credential_000-0001-2345-6789", "{something}");
+ user.setUserAttribute("orcid_user_properties_000-0001-2345-6789", "{something}");
+ user.setUserAttribute("id_something", "12345");
+ user.setUserAttribute("somethingelse", "54321");
+
+ MCRUserManager.createUser(user);
+
+ MODSPersonMigrationCommands.migrateModsperson(userName, "local");
+ user = MCRUserManager.getUser(userName, "local");
+ assertEquals(8, user.getAttributes().size());
+ assertTrue(user.getAttributes().stream().anyMatch(attr -> attr.getName().equals("id_modsperson")));
+ String objId = user.getUserAttribute("id_modsperson");
+
+ assertEquals(1, MODSPersonLookup.getCacheSize());
+
+ MCRObject obj = MCRMetadataManager.retrieveMCRObject(MCRObjectID.getInstance(objId));
+ MCRMODSWrapper wrapper = new MCRMODSWrapper(obj);
+ Element personName = wrapper.getElement("mods:name[@type='personal']");
+
+ Set persons = MODSPersonLookup.lookup(personName);
+ assertNotNull(persons);
+ assertEquals(1, persons.size());
+ MODSPersonLookup.PersonCache person = persons.iterator().next();
+ assertEquals("Peter", person.getGivenName());
+ assertEquals("Tester", person.getFamilyName());
+ Set personKeys = person.getKeys();
+ assertEquals(4, personKeys.size());
+ assertTrue(personKeys.contains("000-0001-2345-6789|orcid"));
+ assertTrue(personKeys.contains("00011112222|scopus"));
+ assertTrue(personKeys.contains("12345|something"));
+ assertTrue(personKeys.contains("54321|somethingelse"));
+ }
+
+ @Test
+ public void testMigrateAll() {
+ MCRUser user;
+ for (int i = 0; i < 30; i++) {
+ String userName = "test" + i;
+ user = new MCRUser(userName, MCRRealmFactory.getLocalRealm());
+ user.setRealName("Tester, Peter" + i);
+ MCRUserManager.createUser(user);
+ }
+
+ for (int i = 30; i < 60; i++) {
+ String userName = "test" + i;
+ user = new MCRUser(userName, MCRRealmFactory.getLocalRealm());
+ user.setRealName("Tester, Petra" + i);
+ user.setUserAttribute("id_connection", UUID.randomUUID().toString());
+ MCRUserManager.createUser(user);
+ }
+
+ List commands = MODSPersonMigrationCommands.migrateAllModsperson();
+ assertEquals(30, commands.size());
+ for (String command : commands) {
+ assertTrue(command.contains("modsperson migration migrate user"));
+ assertTrue(command.contains("in realm local to modsperson"));
+ assertFalse(command.contains("Peter"));
+ }
+ }
+
+ @Test
+ public void testDeleteUsers() {
+ MCRUser user;
+ // no connection id
+ for (int i = 0; i < 10; i++) {
+ String userName = "test" + i;
+ user = new MCRUser(userName, MCRRealmFactory.getLocalRealm());
+ user.setRealName("Tester, Peter" + i);
+ user.setUserAttribute("id_connect", UUID.randomUUID().toString());
+ user.setLastLogin();
+ MCRUserManager.createUser(user);
+ }
+ // no connection id
+ for (int i = 10; i < 20; i++) {
+ String userName = "test" + i;
+ user = new MCRUser(userName, MCRRealmFactory.getLocalRealm());
+ user.setRealName("Tester, Alex" + i);
+ user.setLastLogin();
+ MCRUserManager.createUser(user);
+ }
+ // not local realm
+ for (int i = 20; i < 30; i++) {
+ String userName = "test" + i;
+ user = new MCRUser(userName, MCRRealmFactory.getRealm("test-realm"));
+ user.setRealName("Tester, Lisa" + i);
+ user.setLastLogin();
+ user.setUserAttribute("id_connection", UUID.randomUUID().toString());
+ MCRUserManager.createUser(user);
+ }
+ // no last login
+ for (int i = 30; i < 40; i++) {
+ String userName = "test" + i;
+ user = new MCRUser(userName, MCRRealmFactory.getLocalRealm());
+ user.setRealName("Tester, Hans" + i);
+ user.setUserAttribute("id_connection", UUID.randomUUID().toString());
+ MCRUserManager.createUser(user);
+ }
+ // will be deleted
+ for (int i = 40; i < 50; i++) {
+ String userName = "test" + i;
+ user = new MCRUser(userName, MCRRealmFactory.getLocalRealm());
+ user.setRealName("Tester, Petra" + i);
+ user.setLastLogin();
+ user.setUserAttribute("id_connection", UUID.randomUUID().toString());
+ MCRUserManager.createUser(user);
+ }
+
+ assertEquals(50, MCRUserManager.countUsers(null, null, null, null));
+
+ MODSPersonMigrationCommands.deleteUsersFromDatabase();
+ assertEquals(40, MCRUserManager.countUsers(null, null, null, null));
+ }
+
+ @Test
+ public void deleteConnectionIdsDB() throws IOException, MCRAccessException {
+ MCRUser user;
+ for (int i = 0; i < 30; i++) {
+ String userName = "test" + i;
+ user = new MCRUser(userName, MCRRealmFactory.getLocalRealm());
+ user.setRealName("Tester, Peter" + i);
+ user.setUserAttribute("id_connection", UUID.randomUUID().toString());
+ user.setUserAttribute("id_connect", UUID.randomUUID().toString());
+ user.setUserAttribute("id_scopus", UUID.randomUUID().toString());
+ MCRUserManager.createUser(user);
+ }
+ assertEquals(30, MCRUserManager.countUsers(null, null, null, null));
+
+ MODSPersonMigrationCommands.deleteConnectionIds();
+
+ List allUsers = MCRUserManager.listUsers(null, null, null, null);
+ assertEquals(30, allUsers.size());
+
+ for (MCRUser userAfter : allUsers) {
+ assertEquals(2, userAfter.getAttributes().size());
+ assertNull(userAfter.getUserAttribute("id_connection"));
+ assertNotNull(userAfter.getUserAttribute("id_connect"));
+ assertNotNull(userAfter.getUserAttribute("id_scopus"));
+ }
+ }
+
+ @Test
+ public void deleteConnectionIdsFileSystem() throws JDOMException, IOException, MCRAccessException {
+ URL url1 = MCRObjectMetadataTest.class.getResource(
+ "/MODSPersonMigrationCommandsTest/junit_mods_00000013.xml");
+ Document doc1 = new MCRURLContent(url1).asXML();
+ MCRObject obj1 = new MCRObject(doc1);
+
+ URL url2 = MCRObjectMetadataTest.class.getResource(
+ "/MODSPersonMigrationCommandsTest/junit_mods_00000014.xml");
+ Document doc2 = new MCRURLContent(url2).asXML();
+ MCRObject obj2 = new MCRObject(doc2);
+
+ URL url3 = MCRObjectMetadataTest.class.getResource(
+ "/MODSPersonMigrationCommandsTest/junit_mods_00000015.xml");
+ Document doc3 = new MCRURLContent(url3).asXML();
+ MCRObject obj3 = new MCRObject(doc3);
+
+ MCRMetadataManager.create(obj1);
+ MCRMetadataManager.create(obj2);
+ MCRMetadataManager.create(obj3);
+
+ List allModIds = MCRCommandUtils.getIdsForType("mods").toList();
+ assertEquals(3, allModIds.size());
+
+ MODSPersonMigrationCommands.deleteConnectionIds();
+ allModIds = MCRCommandUtils.getIdsForType("mods").toList();
+ assertEquals(3, allModIds.size());
+
+ for (String allModId : allModIds) {
+ MCRObject o = MCRMetadataManager.retrieveMCRObject(MCRObjectID.getInstance(allModId));
+ List nameIdentifiers = new MCRMODSWrapper(o).getMODS()
+ .getChildren("name", MCRConstants.MODS_NAMESPACE).stream()
+ .flatMap(nameElement -> nameElement.getChildren("nameIdentifier", MCRConstants.MODS_NAMESPACE).stream())
+ .toList();
+ assertTrue(nameIdentifiers.stream()
+ .noneMatch(e -> "connection".equals(e.getAttributeValue("type"))));
+ assertTrue(nameIdentifiers.stream()
+ .anyMatch(e -> "lsf".equals(e.getAttributeValue("type"))));
+ assertTrue(nameIdentifiers.stream()
+ .anyMatch(e -> "scopus".equals(e.getAttributeValue("type"))));
+ }
+ }
+}
diff --git a/ubo-modsperson/src/test/resources/MODSPersonEventHandlerTest/junit_mods_00000017.xml b/ubo-modsperson/src/test/resources/MODSPersonEventHandlerTest/junit_mods_00000017.xml
new file mode 100644
index 000000000..21c71735b
--- /dev/null
+++ b/ubo-modsperson/src/test/resources/MODSPersonEventHandlerTest/junit_mods_00000017.xml
@@ -0,0 +1,19 @@
+
+
+
+
+
+
+
+ This is a test-publication
+
+
+ Müller
+ Peter
+ 99999
+
+
+
+
+
+
diff --git a/ubo-modsperson/src/test/resources/MODSPersonEventHandlerTest/junit_modsperson_00000016.xml b/ubo-modsperson/src/test/resources/MODSPersonEventHandlerTest/junit_modsperson_00000016.xml
new file mode 100644
index 000000000..885177166
--- /dev/null
+++ b/ubo-modsperson/src/test/resources/MODSPersonEventHandlerTest/junit_modsperson_00000016.xml
@@ -0,0 +1,16 @@
+
+
+
+
+
+
+
+ Müller
+ Peter
+ 99999
+
+
+
+
+
+
diff --git a/ubo-modsperson/src/test/resources/MODSPersonEventHandlerTest/junit_modsperson_00000018.xml b/ubo-modsperson/src/test/resources/MODSPersonEventHandlerTest/junit_modsperson_00000018.xml
new file mode 100644
index 000000000..f55e578f5
--- /dev/null
+++ b/ubo-modsperson/src/test/resources/MODSPersonEventHandlerTest/junit_modsperson_00000018.xml
@@ -0,0 +1,16 @@
+
+
+
+
+
+
+
+ Peter
+ Müller
+ 123456
+
+
+
+
+
+
diff --git a/ubo-modsperson/src/test/resources/MODSPersonLinkingEventHandlerTest/junit_mods_00000001.xml b/ubo-modsperson/src/test/resources/MODSPersonLinkingEventHandlerTest/junit_mods_00000001.xml
new file mode 100644
index 000000000..0c3238631
--- /dev/null
+++ b/ubo-modsperson/src/test/resources/MODSPersonLinkingEventHandlerTest/junit_mods_00000001.xml
@@ -0,0 +1,20 @@
+
+
+
+
+
+
+
+ This is a test-publication
+
+
+ Müller
+ Lisa
+ 77777
+ 1112222999
+
+
+
+
+
+
diff --git a/ubo-modsperson/src/test/resources/MODSPersonLinkingEventHandlerTest/junit_mods_00000002.xml b/ubo-modsperson/src/test/resources/MODSPersonLinkingEventHandlerTest/junit_mods_00000002.xml
new file mode 100644
index 000000000..4876f623d
--- /dev/null
+++ b/ubo-modsperson/src/test/resources/MODSPersonLinkingEventHandlerTest/junit_mods_00000002.xml
@@ -0,0 +1,20 @@
+
+
+
+
+
+
+
+ This is a test-publication
+
+
+ Meyer
+ Lena
+ 44444
+ 1112222888
+
+
+
+
+
+
diff --git a/ubo-modsperson/src/test/resources/MODSPersonLinkingEventHandlerTest/junit_mods_00000003.xml b/ubo-modsperson/src/test/resources/MODSPersonLinkingEventHandlerTest/junit_mods_00000003.xml
new file mode 100644
index 000000000..92293c6e7
--- /dev/null
+++ b/ubo-modsperson/src/test/resources/MODSPersonLinkingEventHandlerTest/junit_mods_00000003.xml
@@ -0,0 +1,20 @@
+
+
+
+
+
+
+
+ This is a test-publication
+
+
+ Müller
+ Luisa
+ 99999
+ 444555777
+
+
+
+
+
+
diff --git a/ubo-modsperson/src/test/resources/MODSPersonLinkingEventHandlerTest/junit_mods_00000004.xml b/ubo-modsperson/src/test/resources/MODSPersonLinkingEventHandlerTest/junit_mods_00000004.xml
new file mode 100644
index 000000000..b9668abf9
--- /dev/null
+++ b/ubo-modsperson/src/test/resources/MODSPersonLinkingEventHandlerTest/junit_mods_00000004.xml
@@ -0,0 +1,20 @@
+
+
+
+
+
+
+
+ This is a test-publication
+
+
+ Müller
+ Adam
+ 77766
+ 444000777
+
+
+
+
+
+
diff --git a/ubo-modsperson/src/test/resources/MODSPersonLinkingEventHandlerTest/junit_mods_00000005.xml b/ubo-modsperson/src/test/resources/MODSPersonLinkingEventHandlerTest/junit_mods_00000005.xml
new file mode 100644
index 000000000..3e2100dae
--- /dev/null
+++ b/ubo-modsperson/src/test/resources/MODSPersonLinkingEventHandlerTest/junit_mods_00000005.xml
@@ -0,0 +1,20 @@
+
+
+
+
+
+
+
+ This is a test-publication
+
+
+ Anders
+ Peter
+ 10101
+ 444000777
+
+
+
+
+
+
diff --git a/ubo-modsperson/src/test/resources/MODSPersonLinkingEventHandlerTest/junit_mods_00000006.xml b/ubo-modsperson/src/test/resources/MODSPersonLinkingEventHandlerTest/junit_mods_00000006.xml
new file mode 100644
index 000000000..be2158b24
--- /dev/null
+++ b/ubo-modsperson/src/test/resources/MODSPersonLinkingEventHandlerTest/junit_mods_00000006.xml
@@ -0,0 +1,20 @@
+
+
+
+
+
+
+
+ This is a test-publication
+
+
+ Müller
+ Adam
+ 77788
+ 444000777
+
+
+
+
+
+
diff --git a/ubo-modsperson/src/test/resources/MODSPersonLinkingEventHandlerTest/junit_mods_00000007.xml b/ubo-modsperson/src/test/resources/MODSPersonLinkingEventHandlerTest/junit_mods_00000007.xml
new file mode 100644
index 000000000..e7073d9f9
--- /dev/null
+++ b/ubo-modsperson/src/test/resources/MODSPersonLinkingEventHandlerTest/junit_mods_00000007.xml
@@ -0,0 +1,20 @@
+
+
+
+
+
+
+
+ This is a test-publication
+
+
+ Richter
+ Hans Peter
+ 88888
+ 3332222999
+
+
+
+
+
+
diff --git a/ubo-modsperson/src/test/resources/MODSPersonLinkingEventHandlerTest/junit_mods_00000008.xml b/ubo-modsperson/src/test/resources/MODSPersonLinkingEventHandlerTest/junit_mods_00000008.xml
new file mode 100644
index 000000000..fc1f68e89
--- /dev/null
+++ b/ubo-modsperson/src/test/resources/MODSPersonLinkingEventHandlerTest/junit_mods_00000008.xml
@@ -0,0 +1,20 @@
+
+
+
+
+
+
+
+ This is a test-publication
+
+
+ Richter-Doppelname
+ Hans Peter
+ 88888
+ 3332222999
+
+
+
+
+
+
diff --git a/ubo-modsperson/src/test/resources/MODSPersonLookupTest/junit_mods_00000009.xml b/ubo-modsperson/src/test/resources/MODSPersonLookupTest/junit_mods_00000009.xml
new file mode 100644
index 000000000..e43379b02
--- /dev/null
+++ b/ubo-modsperson/src/test/resources/MODSPersonLookupTest/junit_mods_00000009.xml
@@ -0,0 +1,20 @@
+
+
+
+
+
+
+
+ This is a test-publication
+
+
+ Koch
+ Heinz
+ 77777
+ 555555666
+
+
+
+
+
+
diff --git a/ubo-modsperson/src/test/resources/MODSPersonLookupTest/junit_modsperson_00000010.xml b/ubo-modsperson/src/test/resources/MODSPersonLookupTest/junit_modsperson_00000010.xml
new file mode 100644
index 000000000..a1b76cad4
--- /dev/null
+++ b/ubo-modsperson/src/test/resources/MODSPersonLookupTest/junit_modsperson_00000010.xml
@@ -0,0 +1,25 @@
+
+
+
+
+
+
+
+ Müller
+ Adam
+ 98765
+ 2222222333
+
+ Müller
+ A
+
+
+ Müller Meyer
+ Adam
+
+
+
+
+
+
+
diff --git a/ubo-modsperson/src/test/resources/MODSPersonLookupTest/junit_modsperson_00000011.xml b/ubo-modsperson/src/test/resources/MODSPersonLookupTest/junit_modsperson_00000011.xml
new file mode 100644
index 000000000..eaba1c6ca
--- /dev/null
+++ b/ubo-modsperson/src/test/resources/MODSPersonLookupTest/junit_modsperson_00000011.xml
@@ -0,0 +1,17 @@
+
+
+
+
+
+
+
+ Meyer
+ Gustav
+ 112233
+ 2222222444
+
+
+
+
+
+
diff --git a/ubo-modsperson/src/test/resources/MODSPersonLookupTest/junit_modsperson_00000012.xml b/ubo-modsperson/src/test/resources/MODSPersonLookupTest/junit_modsperson_00000012.xml
new file mode 100644
index 000000000..c6b825dad
--- /dev/null
+++ b/ubo-modsperson/src/test/resources/MODSPersonLookupTest/junit_modsperson_00000012.xml
@@ -0,0 +1,21 @@
+
+
+
+
+
+
+
+ Müller
+ Gustavo
+ 77777
+ 555555666
+
+ Koch
+ Heinz
+
+
+
+
+
+
+
diff --git a/ubo-modsperson/src/test/resources/MODSPersonMigrationCommandsTest/junit_mods_00000013.xml b/ubo-modsperson/src/test/resources/MODSPersonMigrationCommandsTest/junit_mods_00000013.xml
new file mode 100644
index 000000000..9094e92d3
--- /dev/null
+++ b/ubo-modsperson/src/test/resources/MODSPersonMigrationCommandsTest/junit_mods_00000013.xml
@@ -0,0 +1,28 @@
+
+
+
+
+
+
+
+ This is a test-publication
+
+
+ Müller
+ Lisa
+ 77777
+ 1112222999
+ 848c13ef-ece5-4bea-92c5-3cc29dad5bc1
+
+
+ Meyer
+ Lena
+ 88888
+ 1112222888
+ 9573d0ee-0731-4d75-af18-393eedce7adb
+
+
+
+
+
+
diff --git a/ubo-modsperson/src/test/resources/MODSPersonMigrationCommandsTest/junit_mods_00000014.xml b/ubo-modsperson/src/test/resources/MODSPersonMigrationCommandsTest/junit_mods_00000014.xml
new file mode 100644
index 000000000..04c669b25
--- /dev/null
+++ b/ubo-modsperson/src/test/resources/MODSPersonMigrationCommandsTest/junit_mods_00000014.xml
@@ -0,0 +1,21 @@
+
+
+
+
+
+
+
+ This is a test-publication
+
+
+ Müller
+ Peter
+ 55555
+ 1112222555
+ 615cfc5f-89b3-40a5-83b7-efc82c6b3830
+
+
+
+
+
+
diff --git a/ubo-modsperson/src/test/resources/MODSPersonMigrationCommandsTest/junit_mods_00000015.xml b/ubo-modsperson/src/test/resources/MODSPersonMigrationCommandsTest/junit_mods_00000015.xml
new file mode 100644
index 000000000..738c68149
--- /dev/null
+++ b/ubo-modsperson/src/test/resources/MODSPersonMigrationCommandsTest/junit_mods_00000015.xml
@@ -0,0 +1,20 @@
+
+
+
+
+
+
+
+ This is a test-publication
+
+
+ Schmidt
+ Lisa
+ 44444
+ 1112222444
+
+
+
+
+
+
diff --git a/ubo-modsperson/src/test/resources/MODSPersonORCIDLinkingEventHandlerTest/junit_modsperson_00000019.xml b/ubo-modsperson/src/test/resources/MODSPersonORCIDLinkingEventHandlerTest/junit_modsperson_00000019.xml
new file mode 100644
index 000000000..8bf3f8a7c
--- /dev/null
+++ b/ubo-modsperson/src/test/resources/MODSPersonORCIDLinkingEventHandlerTest/junit_modsperson_00000019.xml
@@ -0,0 +1,16 @@
+
+
+
+
+
+
+
+ Peter
+ Müller
+ 123456
+
+
+
+
+
+
diff --git a/ubo-modsperson/src/test/resources/mycore.properties b/ubo-modsperson/src/test/resources/mycore.properties
new file mode 100644
index 000000000..1aaa0c8ee
--- /dev/null
+++ b/ubo-modsperson/src/test/resources/mycore.properties
@@ -0,0 +1,16 @@
+MCR.user2.IdentityManagement.UserCreation.Unvalidated.Realm=local
+MCR.user2.Realms.URI=resource:realms-ubo.xml
+MCR.user2.matching.chain=org.mycore.ubo.matcher.MCRUserMatcherDummy
+MCR.user2.matching.lead_id=lsf
+MCR.user2.matching.publication.connection.strategy=uuid
+MCR.EventHandler.MCRObject.019.Class=
+# trigger MODSPersonLinkingEventHandler manually in tests
+MCR.EventHandler.MCRObject.022.Class=
+MCR.EventHandler.MCRObject.023.Class=org.mycore.ubo.modsperson.MODSPersonEventHandler
+MCR.EventHandler.MCRObject.040.Class=org.mycore.ubo.modsperson.linking.UBOMODSLinksEventHandler
+MCR.EventHandler.MCRUser.021.Class=org.mycore.ubo.modsperson.linking.MODSPersonORCIDLinkingEventHandler
+MCR.Access.Class=org.mycore.access.MCRAccessBaseImpl
+UBO.projectid.default=junit
+
+# Override MCRNameMerger from mycore-mods
+MCR.MODS.Merger.name=org.mycore.ubo.modsperson.merger.MCRNameMerger
diff --git a/ubo-modsperson/src/test/resources/realms-ubo.xml b/ubo-modsperson/src/test/resources/realms-ubo.xml
new file mode 100644
index 000000000..3555bf672
--- /dev/null
+++ b/ubo-modsperson/src/test/resources/realms-ubo.xml
@@ -0,0 +1,8 @@
+
+
+
+
+
+
+
+
diff --git a/ubo-webapp/pom.xml b/ubo-webapp/pom.xml
index 01e8c6db3..d988595b7 100644
--- a/ubo-webapp/pom.xml
+++ b/ubo-webapp/pom.xml
@@ -78,5 +78,11 @@
ubo-common
runtime
+
+ org.mycore.ubo
+ ubo-modsperson
+ ${project.version}
+ runtime
+
|