diff --git a/pom.xml b/pom.xml index 839269fc6..e2047f820 100644 --- a/pom.xml +++ b/pom.xml @@ -419,6 +419,7 @@ ubo-common + ubo-modsperson ubo-webapp ubo-cli diff --git a/ubo-cli/pom.xml b/ubo-cli/pom.xml index 1e268830c..517907f4e 100644 --- a/ubo-cli/pom.xml +++ b/ubo-cli/pom.xml @@ -112,6 +112,12 @@ + + ${project.parent.groupId} + ubo-modsperson + ${project.version} + runtime + org.mycore mycore-ocfl diff --git a/ubo-cli/src/main/setup/classifications/nameIdentifier.xml b/ubo-cli/src/main/setup/classifications/nameIdentifier.xml index 6e8c4a8ec..bbea12930 100644 --- a/ubo-cli/src/main/setup/classifications/nameIdentifier.xml +++ b/ubo-cli/src/main/setup/classifications/nameIdentifier.xml @@ -38,10 +38,10 @@ + + org.mycore.ubo + ubo-modsperson + dev-SNAPSHOT + compile + xml-apis xml-apis diff --git a/ubo-common/src/main/java/org/mycore/ubo/DozBibEntryServlet.java b/ubo-common/src/main/java/org/mycore/ubo/DozBibEntryServlet.java index 18c43d48e..971c36b9c 100644 --- a/ubo-common/src/main/java/org/mycore/ubo/DozBibEntryServlet.java +++ b/ubo-common/src/main/java/org/mycore/ubo/DozBibEntryServlet.java @@ -17,6 +17,7 @@ import org.mycore.common.MCRMailer; import org.mycore.common.config.MCRConfiguration2; import org.mycore.common.content.MCRJDOMContent; +import org.mycore.datamodel.common.MCRLinkTableManager; import org.mycore.datamodel.common.MCRXMLMetadataManager; import org.mycore.datamodel.metadata.MCRMetaLinkID; import org.mycore.datamodel.metadata.MCRMetadataManager; @@ -29,8 +30,10 @@ import org.mycore.mods.MCRMODSWrapper; import java.io.IOException; +import java.util.Collection; import java.util.HashMap; import java.util.List; +import java.util.Locale; public class DozBibEntryServlet extends MCRServlet { @@ -110,6 +113,15 @@ private void deleteEntry(HttpServletRequest req, HttpServletResponse res) throws return; } + MCRLinkTableManager linkTableManager = MCRLinkTableManager.getInstance(); + if (linkTableManager.countReferenceLinkTo(oid) > 0) { + Collection linkedIds = linkTableManager.getSourceOf(oid); + String message = String.format(Locale.ROOT, "Entry still has %d linked entries: %s", + linkedIds.size(), String.join(", ", linkedIds)); + res.sendError(HttpServletResponse.SC_CONFLICT, message); + return; + } + obj.getService().setState("deleted"); Document xml = obj.createXML(); diff --git a/ubo-common/src/main/java/org/mycore/ubo/dedup/DeDupCriteriaBuilder.java b/ubo-common/src/main/java/org/mycore/ubo/dedup/DeDupCriteriaBuilder.java index 3caf7cf32..a76e7dc7b 100644 --- a/ubo-common/src/main/java/org/mycore/ubo/dedup/DeDupCriteriaBuilder.java +++ b/ubo-common/src/main/java/org/mycore/ubo/dedup/DeDupCriteriaBuilder.java @@ -18,6 +18,7 @@ import org.mycore.datamodel.metadata.MCRObjectID; import org.mycore.mods.merger.MCRHyphenNormalizer; import org.mycore.ubo.dedup.jpa.DeduplicationKeyManager; +import org.mycore.ubo.dedup.jpa.DeduplicationKeyManager.DedupType; import java.text.Normalizer; import java.text.Normalizer.Form; @@ -63,7 +64,8 @@ public void updateDeDupCriteria(Element mods, MCRObjectID id) { // add new dedup keys, if any for (DeDupCriterion criteria : getDeDupCriteria(mods)) { - deduplicationKeyManager.addDeduplicationKey(id.toString(), criteria.getType(), criteria.getKey()); + deduplicationKeyManager.addDeduplicationKey(id.toString(), + DedupType.fromString(criteria.getType()), criteria.getKey()); } /* add dedup keys for the host, too or not??? @@ -75,10 +77,34 @@ public void updateDeDupCriteria(Element mods, MCRObjectID id) { } */ } + public void updateDeDupCriteriaPerson(Element modsperson, MCRObjectID id) { + // remove existion dedup keys + for (Element extension : modsperson.getChildren("extension", MCRConstants.MODS_NAMESPACE)) { + extension.removeChildren("dedup"); + if(extension.getChildren().isEmpty()) { + extension.detach(); + } + } + DeduplicationKeyManager deduplicationKeyManager = DeduplicationKeyManager.getInstance(); + + if (modsperson.getName().equals("mods")) { + deduplicationKeyManager.clearDeduplicationKeys(id.toString()); + } + // add new dedup keys, if any + for (DeDupCriterion criteria : getDeDupCriteriaPerson(modsperson)) { + deduplicationKeyManager.addDeduplicationKey(id.toString(), + DedupType.fromString(criteria.getType()), criteria.getKey()); + } + } + private Set getDeDupCriteria(Element mods) { return buildFromMODS(mods); } + private Set getDeDupCriteriaPerson(Element modsPerson) { + return buildFromMODSPerson(modsPerson); + } + /** * Builds deduplication criteria from the given MODS publication metadata */ @@ -102,6 +128,43 @@ public Set buildFromMODS(Element mods) { return criteria; } + /** + * Builds deduplication criteria from the given MODS person metadata + */ + public Set buildFromMODSPerson(Element modsperson) { + Set criteria = new HashSet<>(); + + for (Element identifier : removeEmptyElements(getNodes(modsperson, + "mods:name[@type='personal']/mods:nameIdentifier"))) { + if (identifier.getAttributeValue("type") != null && + !identifier.getAttributeValue("type").equals("connection")) { + criteria.add(buildFromNameIdentifier(identifier)); + } + } + + for (Element name : getNodes(modsperson, "mods:name[@type='personal']")) { + Element familyName = getNodes(name, "mods:namePart[@type='family']").isEmpty() ? + null : getNodes(name, "mods:namePart[@type='family']").get(0); + Element givenName = getNodes(name, "mods:namePart[@type='given']").isEmpty() ? + null : getNodes(name, "mods:namePart[@type='given']").get(0); + if (givenName != null && familyName != null) { + criteria.add(buildFromFullName(givenName, familyName)); + } + + for (Element alternativeName : getNodes(modsperson, "mods:alternativeName")) { + Element altFamilyName = getNodes(alternativeName, "mods:namePart[@type='family']") == null ? + null : getNodes(alternativeName, "mods:namePart[@type='family']").get(0); + Element altGivenName = getNodes(alternativeName, "mods:namePart[@type='given']") == null ? + null : getNodes(alternativeName, "mods:namePart[@type='given']").get(0); + if (altGivenName != null && altFamilyName != null) { + criteria.add(buildFromFullName(altGivenName, altFamilyName)); + } + } + } + + return criteria; + } + /** * Helper method to get a list of nodes by specifying an xPath * @@ -147,6 +210,16 @@ public DeDupCriterion buildFromIdentifier(String type, String value) { return new DeDupCriterion("identifier", type + ":" + value); } + /** + * Builds a deduplication criterion from a mods:nameIdentifier element + */ + public DeDupCriterion buildFromNameIdentifier(Element identifier) { + String type = identifier.getAttributeValue("type"); + String value = identifier.getTextTrim(); + value = value.replaceAll("-", ""); + return new DeDupCriterion("nameidentifier", type + ":" + value); + } + /** * Builds a deduplication criterion for a given mods:shelflocator */ @@ -182,7 +255,7 @@ private String getCombinedTitle(Element modsTitle) { } /** - * Builds a combined deduplication criterion from a title and author elment. + * Builds a combined deduplication criterion from a title and author element. * That means both must match together to identify duplicates. */ public DeDupCriterion buildFromTitleAuthor(String title, String author) { @@ -191,6 +264,16 @@ public DeDupCriterion buildFromTitleAuthor(String title, String author) { return new DeDupCriterion("ta", author + ": " + title); } + /** + * Builds a combined deduplication criterion from a given and family name. + * That means both must match together to identify duplicates. + */ + public DeDupCriterion buildFromFullName(Element givenNameModsPart, Element familyNameModsPart) { + String givenName = normalizeText(givenNameModsPart.getTextTrim()); + String familyName = normalizeText(familyNameModsPart.getTextTrim()); + return new DeDupCriterion("name", givenName + ":" + familyName); + } + /** * Normalizes text to be fault tolerant when matching for duplicates. * Accents, umlauts, case are normalized. Punctuation and non-alphabetic/non-digit characters are removed. diff --git a/ubo-common/src/main/java/org/mycore/ubo/dedup/DeDupEventHandler.java b/ubo-common/src/main/java/org/mycore/ubo/dedup/DeDupEventHandler.java index 124226648..ac94c0235 100644 --- a/ubo-common/src/main/java/org/mycore/ubo/dedup/DeDupEventHandler.java +++ b/ubo-common/src/main/java/org/mycore/ubo/dedup/DeDupEventHandler.java @@ -30,15 +30,22 @@ protected void handleObjectRepaired(MCREvent evt, MCRObject obj) { @Override protected void handleObjectDeleted(MCREvent evt, MCRObject obj) { - LOGGER.info("clearing deduplication keys and no duplicates for object " + obj.getId().toString()); + LOGGER.info("clearing deduplication keys and no duplicates for object " + obj.getId()); DeduplicationKeyManager.getInstance().clearDeduplicationKeys(obj.getId().toString()); DeduplicationKeyManager.getInstance().clearNoDuplicates(obj.getId().toString()); } private void updateDeDupCriteria(MCRObject obj) { - LOGGER.info("updating deduplication keys for object " + obj.getId().toString()); + if (!("mods".equals(obj.getId().getTypeId()) || "modsperson".equals(obj.getId().getTypeId()))) { + return; + } + LOGGER.info("updating deduplication keys for object " + obj.getId()); Element mods = new MCRMODSWrapper(obj).getMODS(); - new DeDupCriteriaBuilder().updateDeDupCriteria(mods, obj.getId()); + if ("mods".equals(obj.getId().getTypeId())) { + new DeDupCriteriaBuilder().updateDeDupCriteria(mods, obj.getId()); + } else { + new DeDupCriteriaBuilder().updateDeDupCriteriaPerson(mods, obj.getId()); + } } } diff --git a/ubo-common/src/main/java/org/mycore/ubo/dedup/DeDupResource.java b/ubo-common/src/main/java/org/mycore/ubo/dedup/DeDupResource.java index 35d7a1b31..7e29cacb3 100644 --- a/ubo-common/src/main/java/org/mycore/ubo/dedup/DeDupResource.java +++ b/ubo-common/src/main/java/org/mycore/ubo/dedup/DeDupResource.java @@ -12,6 +12,7 @@ import jakarta.ws.rs.core.Response; import org.mycore.ubo.AccessControl; import org.mycore.ubo.dedup.jpa.DeduplicationKeyManager; +import org.mycore.ubo.dedup.jpa.DeduplicationKeyManager.DedupType; import org.mycore.ubo.dedup.jpa.DeduplicationNoDuplicate; import java.util.List; @@ -23,28 +24,56 @@ public class DeDupResource { @Path("list/duplicates") @Produces(MediaType.APPLICATION_JSON) public Response listDedup(@QueryParam("idSort") String idSortStr, @QueryParam("typeSort") String typeSortStr, @QueryParam("type") String type) { + DedupType[] filter = DeduplicationKeyManager.PUBLICATION_CATEGORY_GROUP; + if (type != null) { + filter = new DedupType[] { + DedupType.fromString(type) }; + } + return listDedupIntern(idSortStr, typeSortStr, filter); + } + + @GET + @Path("list/duplicates-person") + @Produces(MediaType.APPLICATION_JSON) + public Response listDedupPerson(@QueryParam("idSort") String idSortStr, @QueryParam("typeSort") String typeSortStr, @QueryParam("type") String type) { + DedupType[] filter = DeduplicationKeyManager.PERSON_CATEGORY_GROUP; + if (type != null) { + filter = new DedupType[] { + DedupType.fromString(type) }; + } + return listDedupIntern(idSortStr, typeSortStr, filter); + } + + /** + * Returns a sorted list of all duplicates of the given deduplication types. + * @param idSortStr sort order of id field (asc, desc, none) + * @param typeSortStr sort order of type field (asc, desc, none) + * @param dedupTypes all {@link DeduplicationKeyManager.DedupType dedup types} that should be searched for + * @return the dedup list + */ + private Response listDedupIntern(String idSortStr, String typeSortStr, DedupType[] dedupTypes) { if (!AccessControl.currentUserIsAdmin()) { return Response.status(Response.Status.FORBIDDEN).build(); } DeduplicationKeyManager.SortOrder idSort = idSortStr == null ? DeduplicationKeyManager.SortOrder.NONE : - switch (idSortStr) { - case "asc" -> DeduplicationKeyManager.SortOrder.ASC; - case "desc" -> DeduplicationKeyManager.SortOrder.DESC; - case "none" -> DeduplicationKeyManager.SortOrder.NONE; - default -> throw new BadRequestException("Invalid sort order: " + idSortStr); - }; + switch (idSortStr) { + case "asc" -> DeduplicationKeyManager.SortOrder.ASC; + case "desc" -> DeduplicationKeyManager.SortOrder.DESC; + case "none" -> DeduplicationKeyManager.SortOrder.NONE; + default -> throw new BadRequestException("Invalid sort order: " + idSortStr); + }; DeduplicationKeyManager.SortOrder typeSort = typeSortStr == null ? DeduplicationKeyManager.SortOrder.NONE : - switch (typeSortStr) { - case "asc" -> DeduplicationKeyManager.SortOrder.ASC; - case "desc" -> DeduplicationKeyManager.SortOrder.DESC; - case "none" -> DeduplicationKeyManager.SortOrder.NONE; - default -> throw new BadRequestException("Invalid sort order: " + typeSortStr); - }; - - List duplicates = DeduplicationKeyManager.getInstance().getDuplicates(idSort, typeSort, type); + switch (typeSortStr) { + case "asc" -> DeduplicationKeyManager.SortOrder.ASC; + case "desc" -> DeduplicationKeyManager.SortOrder.DESC; + case "none" -> DeduplicationKeyManager.SortOrder.NONE; + default -> throw new BadRequestException("Invalid sort order: " + typeSortStr); + }; + + List duplicates = DeduplicationKeyManager.getInstance().getDuplicates(idSort, typeSort, dedupTypes); String json = new Gson().toJson(duplicates.toArray(), PossibleDuplicate[].class); return Response.ok(json) .header("Access-Control-Allow-Origin", "*") diff --git a/ubo-common/src/main/java/org/mycore/ubo/dedup/jpa/DeduplicationCriterionResolver.java b/ubo-common/src/main/java/org/mycore/ubo/dedup/jpa/DeduplicationCriterionResolver.java index 872a39807..17034c629 100644 --- a/ubo-common/src/main/java/org/mycore/ubo/dedup/jpa/DeduplicationCriterionResolver.java +++ b/ubo-common/src/main/java/org/mycore/ubo/dedup/jpa/DeduplicationCriterionResolver.java @@ -11,6 +11,7 @@ import org.mycore.mods.MCRMODSWrapper; import org.mycore.ubo.dedup.DeDupCriteriaBuilder; import org.mycore.ubo.dedup.DeDupCriterion; +import org.mycore.ubo.dedup.jpa.DeduplicationKeyManager.DedupType; import javax.xml.transform.Source; import javax.xml.transform.TransformerException; @@ -54,7 +55,8 @@ public Source resolve(String href, String base) throws TransformerException { } String relation = parts[2]; - if(!Objects.equals("base", relation) && Objects.equals("parent", relation) && Objects.equals("host", relation)) { + if(!(Objects.equals("base", relation) || Objects.equals("parent", relation) + || Objects.equals("host", relation) || Objects.equals("person", relation))) { LOGGER.error("Unknown relation: {} in {}", relation, href); return null; } @@ -80,9 +82,10 @@ public Source resolve(String href, String base) throws TransformerException { if(Objects.equals("base", relation) || Objects.equals("parent", relation)) { Set criteria = deDupCriteriaBuilder.buildFromMODS(mods); criteria.forEach(criterion -> { - possibleDuplicates.addAll(DeduplicationKeyManager.getInstance().getDuplicates(id.toString(), criterion.getType(), criterion.getKey())); + possibleDuplicates.addAll(DeduplicationKeyManager.getInstance().getDuplicates(id.toString(), + Objects.requireNonNull(DedupType.fromString(criterion.getType())), criterion.getKey())); }); - } else { + } else if (Objects.equals("host", relation)) { for (Element host : deDupCriteriaBuilder.getNodes(mods, "mods:relatedItem[@type='host']")) { String externalID = host.getAttributeValue("href", MCRConstants.XLINK_NAMESPACE); if(externalID != null) { @@ -90,9 +93,16 @@ public Source resolve(String href, String base) throws TransformerException { continue; } deDupCriteriaBuilder.buildFromMODS(host).forEach(criterion -> { - possibleDuplicates.addAll(DeduplicationKeyManager.getInstance().getDuplicates(id.toString(), criterion.getType(), criterion.getKey())); + possibleDuplicates.addAll(DeduplicationKeyManager.getInstance().getDuplicates(id.toString(), + Objects.requireNonNull(DedupType.fromString(criterion.getType())), criterion.getKey())); }); } + } else { + Set criteria = deDupCriteriaBuilder.buildFromMODSPerson(mods); + criteria.forEach(criterion -> { + possibleDuplicates.addAll(DeduplicationKeyManager.getInstance().getDuplicates(id.toString(), + Objects.requireNonNull(DedupType.fromString(criterion.getType())), criterion.getKey())); + }); } possibleDuplicates diff --git a/ubo-common/src/main/java/org/mycore/ubo/dedup/jpa/DeduplicationKeyManager.java b/ubo-common/src/main/java/org/mycore/ubo/dedup/jpa/DeduplicationKeyManager.java index f3a1298c3..8c0e608cb 100644 --- a/ubo-common/src/main/java/org/mycore/ubo/dedup/jpa/DeduplicationKeyManager.java +++ b/ubo-common/src/main/java/org/mycore/ubo/dedup/jpa/DeduplicationKeyManager.java @@ -4,6 +4,7 @@ import jakarta.persistence.criteria.CriteriaBuilder; import jakarta.persistence.criteria.CriteriaQuery; import jakarta.persistence.criteria.Order; +import jakarta.persistence.criteria.Path; import jakarta.persistence.criteria.Predicate; import jakarta.persistence.criteria.Root; import jakarta.persistence.criteria.Subquery; @@ -19,6 +20,9 @@ public class DeduplicationKeyManager { + public static final DedupType[] PUBLICATION_CATEGORY_GROUP = { DedupType.TA, DedupType.SHELFMARK, DedupType.IDENTIFIER}; + public static final DedupType[] PERSON_CATEGORY_GROUP = { DedupType.NAME, DedupType.NAME_IDENTIFIER}; + public DeduplicationKeyManager() { } @@ -50,16 +54,18 @@ public void clearNoDuplicates(String mcrId) { .executeUpdate(); } - public void addDeduplicationKey(String mcrId, String type, String key) { + public void addDeduplicationKey(String mcrId, DedupType type, String key) { EntityManager em = MCREntityManagerProvider.getCurrentEntityManager(); DeduplicationKey deduplicationKey = new DeduplicationKey(); deduplicationKey.setMcrId(mcrId); - deduplicationKey.setDeduplicationType(type); + if (type != null) { + deduplicationKey.setDeduplicationType(type.getName()); + } deduplicationKey.setDeduplicationKey(key); em.persist(deduplicationKey); } - public List getDuplicates(SortOrder idSort, SortOrder typeSort, String duplicationTypeFilter) { + public List getDuplicates(SortOrder idSort, SortOrder typeSort, DedupType[] duplicationTypeFilter) { EntityManager em = MCREntityManagerProvider.getCurrentEntityManager(); CriteriaBuilder cb = em.getCriteriaBuilder(); @@ -90,10 +96,16 @@ public List getDuplicates(SortOrder idSort, SortOrder typeSor predicates.add(cb.lessThan(dk1.get(DeduplicationKey_.mcrId), dk2.get(DeduplicationKey_.mcrId))); predicates.add(cb.not(cb.exists(subquery))); - if (duplicationTypeFilter != null) { - predicates.add(cb.equal(dk1.get(DeduplicationKey_.deduplicationType), duplicationTypeFilter)); - } + if (duplicationTypeFilter != null && duplicationTypeFilter.length > 0) { + Path dedupTypePath = dk1.get(DeduplicationKey_.deduplicationType); + CriteriaBuilder.In inClause = cb.in(dedupTypePath); + + for (DedupType type : duplicationTypeFilter) { + inClause.value(type.getName()); + } + predicates.add(inClause); + } Predicate duplicateCondition = cb.and(predicates.toArray(new Predicate[0])); @@ -132,7 +144,7 @@ public List getDuplicates(SortOrder idSort, SortOrder typeSor return resultList.stream().map(o -> new PossibleDuplicate((String) o[0], (String) o[1], (String) o[2], (String) o[3])).toList(); } - public List getDuplicates(String mcrId, String type, String key) { + public List getDuplicates(String mcrId, DedupType type, String key) { EntityManager em = MCREntityManagerProvider.getCurrentEntityManager(); CriteriaBuilder cb = em.getCriteriaBuilder(); @@ -158,7 +170,7 @@ public List getDuplicates(String mcrId, String type, String ke Predicate duplicateCondition = cb.and( cb.equal(dk1.get(DeduplicationKey_.deduplicationKey), key), - cb.equal(dk1.get(DeduplicationKey_.deduplicationType), type), + cb.equal(dk1.get(DeduplicationKey_.deduplicationType), type.getName()), cb.notEqual(dk1.get(DeduplicationKey_.mcrId), mcrId), cb.not(dk1.get(DeduplicationKey_.mcrId).in(subquery1)), cb.not(dk1.get(DeduplicationKey_.mcrId).in(subquery2)) @@ -224,4 +236,34 @@ public enum DeduplicationNoDuplicateOrderFields { MCR_ID_1, MCR_ID_2, CREATOR, DATE; } + /** + * Lists all possible depublication key types + */ + public enum DedupType { + TA("ta"), + SHELFMARK("shelfmark"), + IDENTIFIER("identifier"), + NAME("name"), + NAME_IDENTIFIER("nameidentifier"); + + private final String name; + + DedupType(String name) { + this.name = name; + } + + public String getName() { + return name; + } + + public static DedupType fromString(String text) { + for (DedupType d : DedupType.values()) { + if (d.name.equalsIgnoreCase(text)) { + return d; + } + } + return null; + } + } + } diff --git a/ubo-common/src/main/java/org/mycore/ubo/modsperson/MODSPersonSearchService.java b/ubo-common/src/main/java/org/mycore/ubo/modsperson/MODSPersonSearchService.java new file mode 100644 index 000000000..63b6ebf1a --- /dev/null +++ b/ubo-common/src/main/java/org/mycore/ubo/modsperson/MODSPersonSearchService.java @@ -0,0 +1,120 @@ +package org.mycore.ubo.modsperson; + +import org.apache.solr.client.solrj.SolrQuery; +import org.apache.solr.client.solrj.SolrServerException; +import org.apache.solr.client.solrj.response.QueryResponse; +import org.apache.solr.common.SolrDocument; +import org.apache.solr.common.SolrDocumentList; +import org.jdom2.Element; +import org.mycore.common.MCRException; +import org.mycore.common.config.MCRConfiguration2; +import org.mycore.solr.MCRSolrCoreManager; +import org.mycore.solr.MCRSolrUtils; +import org.mycore.ubo.picker.IdentityService; +import org.mycore.ubo.picker.PersonSearchResult; + +import javax.naming.OperationNotSupportedException; +import java.io.IOException; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; + +/** + * {@link IdentityService} for search by modsperson. Activate by configuration: + *
MCR.IdentityPicker.strategy=org.mycore.ubo.modsperson.MODSPersonSearch
+ * Uses a Solr search with object-type "modsperson" and field "person" for query. + */ +public class MODSPersonSearchService implements IdentityService { + + private final String LEAD_ID = MCRConfiguration2.getStringOrThrow("MCR.user2.matching.lead_id"); + + @Override + public Element getPersonDetails(Map paramMap) { + return null; + } + + @Override + public Element searchPerson(Map paramMap) { + try { + SolrDocumentList searchResults = searchPersonSolr(paramMap.get("lastName") + ", " + paramMap.get("firstName")); + final Element resultsElement = new Element("results"); + for (SolrDocument doc : searchResults) { + resultsElement.addContent(createPersonElement(doc)); + } + return resultsElement; + } catch (SolrServerException | IOException e) { + throw new MCRException("There was an error while searching for: " + paramMap.get("lastName") + ", " + paramMap.get("firstName"), e); + } + } + + @Override + public PersonSearchResult searchPerson(String query) throws OperationNotSupportedException { + try { + SolrDocumentList results = searchPersonSolr(query); + + List personResults = new ArrayList<>(); + + for (SolrDocument doc : results) { + PersonSearchResult.PersonResult result = new PersonSearchResult.PersonResult(this); + + List persons = (List) doc.getFieldValue("person"); + if (persons != null && !persons.isEmpty()) { + result.displayName = persons.get(0); + if (result.displayName.split(",").length > 1) { + result.lastName = result.displayName.split(",")[0].trim(); + result.firstName = result.displayName.split(",")[1].trim(); + } + else if (result.displayName.split(" ").length > 1) { + result.firstName = result.displayName.split(" ")[0].trim(); + result.lastName = result.displayName.split(" ")[1].trim(); + } + } + List pids = ((List) doc.getFieldValue("nid_" + LEAD_ID)); + if (pids != null && !pids.isEmpty()) { + result.pid = pids.get(0); + } + personResults.add(result); + } + + PersonSearchResult personSearchResult = new PersonSearchResult(); + personSearchResult.count = personResults.size(); + personSearchResult.personList = personResults; + return personSearchResult; + } catch (SolrServerException | IOException e) { + throw new MCRException("There was an error while searching for: " + query, e); + } + } + + private SolrDocumentList searchPersonSolr(String searchName) throws SolrServerException, IOException { + final SolrQuery query = new SolrQuery("objectType:modsperson AND person:\"" + MCRSolrUtils.escapeSearchValue(searchName) + "\"") + .setFields("nid_lsf, person").setRows(100).setParam("wt", "json"); + + QueryResponse queryResponse = MCRSolrCoreManager.getMainSolrClient().query(query); + return queryResponse.getResults(); + } + + private Element createPersonElement(SolrDocument doc) { + final Element person = new Element("person"); + + final Element idElement = new Element("id"); + idElement.addContent(((List) doc.getFieldValue("nid_" + LEAD_ID)).get(0)); + person.addContent(idElement); + + final Element realNameElement = new Element("realName"); + final String realName = ((List) doc.getFieldValue("person")).get(0); + realNameElement.addContent(realName); + person.addContent(realNameElement); + + final String[] realNameSplit = realName.split(" ", 2); + + final Element elementFirstName = new Element("firstName"); + elementFirstName.addContent(realNameSplit.length > 1 ? realNameSplit[1] : null); + person.addContent(elementFirstName); + + final Element elementLastName = new Element("lastName"); + elementLastName.addContent(realNameSplit[0]); + person.addContent(elementLastName); + + return person; + } +} diff --git a/ubo-common/src/main/java/org/mycore/ubo/relations/RelationEditorServlet.java b/ubo-common/src/main/java/org/mycore/ubo/relations/RelationEditorServlet.java index d348a1cbd..589d28240 100644 --- a/ubo-common/src/main/java/org/mycore/ubo/relations/RelationEditorServlet.java +++ b/ubo-common/src/main/java/org/mycore/ubo/relations/RelationEditorServlet.java @@ -18,9 +18,16 @@ import org.mycore.frontend.servlets.MCRServletJob; 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.AccessControl; import org.mycore.ubo.DozBibEntryServlet; import org.mycore.ubo.dedup.jpa.DeduplicationKeyManager; +import org.mycore.ubo.modsperson.merger.MCRNameMerger; + +import java.io.IOException; +import java.util.Date; +import java.util.List; import java.io.IOException; import java.util.Date; @@ -62,7 +69,11 @@ public void doGetPost(MCRServletJob job) throws Exception { if (!res.isCommitted()) { String base = req.getParameter("base"); - res.sendRedirect("DozBibEntryServlet?id=" + base + "&XSL.Style=structure"); + if (base.contains("_mods_")) { + res.sendRedirect("DozBibEntryServlet?id=" + base + "&XSL.Style=structure"); + } else { + res.sendRedirect("DozBibEntryServlet?id=" + base + "&XSL.Style=modsperson-structure"); + } } } } @@ -190,10 +201,20 @@ private void mergeMetadata(HttpServletRequest req, HttpServletResponse res) thro MCRObject objInto = MCRMetadataManager.retrieveMCRObject(intoID); MCRObject objFrom = MCRMetadataManager.retrieveMCRObject(fromID); - Element modsToMergeInto = new MCRMODSWrapper(objInto).getMODS(); - Element modsToMergeFrom = new MCRMODSWrapper(objFrom).getMODS(); - MCRMergeTool.merge(modsToMergeInto, modsToMergeFrom); - + Element modsToMergeFrom, modsToMergeInto; + if (objFrom.getId().getTypeId().equals("mods")) { + modsToMergeInto = new MCRMODSWrapper(objInto).getMODS(); + modsToMergeFrom = new MCRMODSWrapper(objFrom).getMODS(); + MCRMergeTool.merge(modsToMergeInto, modsToMergeFrom); + } else { // modsperson + modsToMergeInto = new MCRMODSWrapper(objInto).getElements("mods:name[@type='personal']").get(0); + modsToMergeFrom = new MCRMODSWrapper(objFrom).getElements("mods:name[@type='personal']").get(0); + MCRMergeTool.merge(modsToMergeInto, modsToMergeFrom); + // Set name from mergeFrom as alternativeName in mergeInto + MCRMerger mergeInto = MCRMergerFactory.buildFrom(modsToMergeInto); + MCRMerger mergeFrom = MCRMergerFactory.buildFrom(modsToMergeFrom); + ((MCRNameMerger) mergeInto).mergeAsAlternativeName(mergeFrom); + } boolean preview = "true".equals(req.getParameter("preview")); if (preview) { MCRContent output = new MCRJDOMContent( objInto.createXML() ); diff --git a/ubo-common/src/main/resources/META-INF/resources/import-editor.xed b/ubo-common/src/main/resources/META-INF/resources/import-editor.xed index 054ecd47f..0913f2766 100644 --- a/ubo-common/src/main/resources/META-INF/resources/import-editor.xed +++ b/ubo-common/src/main/resources/META-INF/resources/import-editor.xed @@ -81,9 +81,20 @@ - - + + + + + + + + + + + + + @@ -496,6 +507,14 @@ + + +
+ + +
+
+
diff --git a/ubo-common/src/main/resources/META-INF/resources/navigation.xml b/ubo-common/src/main/resources/META-INF/resources/navigation.xml index 5cad43f8d..6a29f80bf 100644 --- a/ubo-common/src/main/resources/META-INF/resources/navigation.xml +++ b/ubo-common/src/main/resources/META-INF/resources/navigation.xml @@ -16,6 +16,14 @@ + + + + + + + + diff --git a/ubo-common/src/main/resources/META-INF/resources/search-person.xed b/ubo-common/src/main/resources/META-INF/resources/search-person.xed new file mode 100644 index 000000000..aa35e9551 --- /dev/null +++ b/ubo-common/src/main/resources/META-INF/resources/search-person.xed @@ -0,0 +1,117 @@ + + + + Universitätsbibliographie Online: Personensuche + University Bibliography Online: Person search + +
+
+ + + + + + + +
+ + + + + + +
+ + + Alle Bedingungen werden UND-verknüpft. Durch klicken auf + + können Sie ein Suchfeld wiederholen! + + + All search terms are connected with AND. By clicking on + + you can repeat a search field! + + +
+ + + + + + + + + + + + + + +
+
+ + + +
+ + + + insert remove +
+
+ + +
+
+ + + +
+ + + + insert remove +
+
+ +
+ + + + + +
+ +
+ + + +
+
+
+
+ + + + +
+
+
+ +
diff --git a/ubo-common/src/main/resources/config/ubo-common/messages_de.properties b/ubo-common/src/main/resources/config/ubo-common/messages_de.properties index 34e822495..06638cbcc 100644 --- a/ubo-common/src/main/resources/config/ubo-common/messages_de.properties +++ b/ubo-common/src/main/resources/config/ubo-common/messages_de.properties @@ -95,6 +95,7 @@ button.create = Einrichten button.delete = L\u00F6schen button.deleteFileSelected = markierte Dateien l\u00F6schen button.deleteFilespace = Dateibereich l\u00F6schen +button.deletePerson = Diese Person l\u00F6schen? button.deletePublication = Diese Publikation l\u00F6schen? button.deleteYes = Ja, l\u00F6schen! button.edit = Bearbeiten @@ -174,6 +175,7 @@ day.wednesday = Mittwoch dedup.nav.DedupListView.filter = Filter dedup.nav.DedupListView.filter.all = Alle dedup.nav.DedupListView.filter.identifier = Identifikatoren +dedup.nav.DedupListView.filter.name = Name dedup.nav.DedupListView.filter.ta = Titel und Autoren dedup.nav.DedupListView.go.metadata = \u00D6ffne Metadaten dedup.nav.DedupListView.go.stuctureEditor = \u00D6ffne Struktureditor @@ -188,6 +190,7 @@ dedup.nav.NoDuplicateListView.table.mcrId1 = Dokument 1 dedup.nav.NoDuplicateListView.table.mcrId2 = Dokument 2 dedup.nav.doublettes = Dubletten dedup.nav.noDoublettes = Keine Dubletten +dedup.nav.person.doublettes = Personen Dubletten derivate = Dateibereich derivate.confirm.delete = L\u00F6schen best\u00E4tigen derivate.confirm.derivate.delete = Der Dateibereich und alle dazugeh\u00F6rigen Dateien werden gel\u00F6scht @@ -372,8 +375,10 @@ edit.lul.edit = Lehr- und Lernmater edit.media = *Medientyp(en) edit.modified = Ge\u00E4ndert am edit.name = \ Nachname, Vorname -edit.nameFirst = *Vorname -edit.nameLast = *Nachname +edit.nameFirst = * Vorname +edit.nameLast = * Nachname +edit.nameFirstAlternative = alternativer Vorname +edit.nameLastAlternative = alternativer Nachname edit.person = *Person(en) edit.phone = *Telefon edit.pub = Neue Publikation eingeben @@ -707,6 +712,7 @@ result.distributed.search = Suche: result.documentMany = Es wurden {0} Dokumente gefunden. result.documentNo = Es wurden keine auf Ihre Suchanfrage passenden Dokumente gefunden! result.documentOne = Es wurde ein Dokument gefunden. +result.dozbib.alternativeName = Alternativer Name result.dozbib.buttonPDF = als PDF result.dozbib.buttonSelect = w\u00E4hlen result.dozbib.by.faculty = nach Fakult\u00E4ten @@ -731,6 +737,7 @@ result.dozbib.export = oder diese Liste ex result.dozbib.faculties = Fakult\u00E4ten result.dozbib.fulltext = Volltext in DuEPublico result.dozbib.info = Details +result.dozbib.name = Name result.dozbib.now = Wie geht es nun weiter? result.dozbib.nowAccess = Erst dann ist der Eintrag \u00FCber die Suche auffindbar und allgemein zug\u00E4nglich. result.dozbib.nowAdditional = false @@ -740,6 +747,9 @@ result.dozbib.nowSavedStrong = aber noch nicht \u0 result.dozbib.number = Nr. result.dozbib.page = S. result.dozbib.pdfpage = Seite +result.dozbib.personMany = Personen gefunden. +result.dozbib.personNo = Keine den Suchkriterien entsprechenden Personen gefunden. +result.dozbib.personOne = Eine Person gefunden. result.dozbib.publicationMany = Publikationen gefunden. result.dozbib.publicationNo = Keine den Suchkriterien entsprechenden Publikationen gefunden. result.dozbib.publicationOne = Eine Publikation gefunden. @@ -848,6 +858,7 @@ search.media = Medientyp search.next = Weiter search.only = Suche nur in search.or = oder +search.personname = Name search.pub = Suche nach Online-Publikationen in DuEPublico search.select = (bitte w\u00E4hlen) search.select.autocomplete = (bitte eingeben oder w\u00E4hlen) @@ -910,7 +921,10 @@ structure.editor.extractHostLink = als separaten Eintr structure.editor.limit = \ Es werden nur die ersten structure.editor.limitDisplay = \ angezeigt. structure.editor.linked = \ Publikation(en) verkn\u00FCpft. +structure.editor.mergePerson = Pers\u00F6nliche Daten dieser Dublette +structure.editor.mergeIntoPerson = in dieser Person structure.editor.markAsFalseDuplicate = Die Publikation +structure.editor.markAsFalseDuplicatePerson = Die Person structure.editor.markAsFalseDuplicateOf = als keine Dublette von structure.editor.markAsFalseDuplicateQMark = markieren? structure.editor.merge = Titeldaten dieser Dublette @@ -1068,6 +1082,7 @@ ubo.mailreport.subject = Neue Eintr\u00E4ge ubo.mediaType = Datentr\u00E4gertyp ubo.more = mehr ubo.nameIdentifier.orcid.validation = ORCIDs m\u00FCssen die Form 0000-0000-0000-0000 haben! +ubo.newPerson = Neue Person erfassen: ubo.newPublication = Neue Publikation erfassen: ubo.newPublicationWizard = Publikation melden ubo.newPublicationWizard.author.placeholder = Bitte nur den Nachnamen des/r ersten Autors*in eingeben @@ -1267,6 +1282,9 @@ user.profile.publications.ubo.intro = Die Universit\u00E4 user.profile.publications.ubo.outro = \ mit diesem Profil eindeutig zugeordneter LSF ID user.profile.publications.ubo.outro.plural.false = \ mit Ihnen eindeutig zugeordneter LSF ID user.profile.publications.ubo.outro.plural.true = \ mit Ihnen eindeutig zugeordneter LSF ID +user.profile.publications.ubo.outro.modsperson = \ die dieser Person zugeordnet ist +user.profile.publications.ubo.outro.plural.modsperson.true = \ die dieser Person zugeordnet sind +user.profile.publications.ubo.outro.plural.modsperson.false = \ die dieser Person zugeordnet ist user.profile.publications.ubo.published.extro.plural.false = im Status redaktionell gepr\u00FCft. user.profile.publications.ubo.published.extro.plural.true = im Status redaktionell gepr\u00FCft. user.profile.publications.ubo.published.intro.plural.false = Davon ist diff --git a/ubo-common/src/main/resources/config/ubo-common/messages_en.properties b/ubo-common/src/main/resources/config/ubo-common/messages_en.properties index f6d27a7e4..b9b1d73fd 100644 --- a/ubo-common/src/main/resources/config/ubo-common/messages_en.properties +++ b/ubo-common/src/main/resources/config/ubo-common/messages_en.properties @@ -95,6 +95,7 @@ button.create = Create button.delete = Delete button.deleteFileSelected = Delete selected files button.deleteFilespace = Delete filespace +button.deletePerson = Delete this person? button.deletePublication = Delete this publication? button.deleteYes = Yes, delete! button.edit = Edit @@ -173,6 +174,7 @@ day.wednesday = Wednesday dedup.nav.DedupListView.filter = Filter dedup.nav.DedupListView.filter.all = All dedup.nav.DedupListView.filter.identifier = Identifiers +dedup.nav.DedupListView.filter.name = Person Name dedup.nav.DedupListView.filter.ta = Title and Authors dedup.nav.DedupListView.go.metadata = Open Metadata dedup.nav.DedupListView.go.stuctureEditor = Open Structure Editor @@ -187,6 +189,7 @@ dedup.nav.NoDuplicateListView.table.mcrId1 = Document 1 dedup.nav.NoDuplicateListView.table.mcrId2 = Document 2 dedup.nav.doublettes = Duplicates dedup.nav.noDoublettes = No duplicates +dedup.nav.person.doublettes = Person Duplicates derivate = Derivate derivate.confirm.delete = Confirm delete derivate.date.created = Filespace created on @@ -369,10 +372,12 @@ edit.lul.edit = Edit teaching and l edit.media = *Media type(s) edit.modified = Changed on edit.name = \ Lastname, firstname -edit.nameFirst = *First name -edit.nameLast = *Lastname -edit.person = *Person(s) -edit.phone = *Phone +edit.nameFirst = * First name +edit.nameLast = * Lastname +edit.nameFirstAlternative = alternative first name +edit.nameLastAlternative = alternative last name +edit.person = * Person(s) +edit.phone = * Phone edit.pub = Enter new publication edit.pub.edit = Edit publication edit.publication = *Publication @@ -699,6 +704,7 @@ result.distributed.search = Search: result.documentMany = {0} documents found. result.documentNo = No documents found! result.documentOne = One document found. +result.dozbib.alternativeName = Alternative Name result.dozbib.buttonPDF = as PDF result.dozbib.buttonSelect = select result.dozbib.by.faculty = by faculty @@ -723,6 +729,7 @@ result.dozbib.export = or export this list result.dozbib.faculties = faculties result.dozbib.fulltext = Full text in DuEPublico result.dozbib.info = Details +result.dozbib.name = Name result.dozbib.now = How does it go on now? result.dozbib.nowAccess = Only then the entry is searchable and generally accessible. result.dozbib.nowAdditional = false @@ -732,6 +739,9 @@ result.dozbib.nowSavedStrong = but is searchable n result.dozbib.number = no. result.dozbib.page = p. result.dozbib.pdfpage = Page +result.dozbib.personMany = people found. +result.dozbib.personNo = No people corresponding to the searching term found. +result.dozbib.personOne = One Person found. result.dozbib.publicationMany = publications found. result.dozbib.publicationNo = No publications corresponding to the searching term found. result.dozbib.publicationOne = One publication found. @@ -837,6 +847,7 @@ search.media = Media type search.next = Next search.only = Search only in search.or = or +search.personname = Name search.pub = Search for online publications in DuEPublico search.select = (please select) search.select.autocomplete = (type ahead or select) @@ -900,10 +911,13 @@ structure.editor.limit = \ Only the first structure.editor.limitDisplay = \ are displayed. structure.editor.linked = \ publication(s) linked. structure.editor.markAsFalseDuplicate = Mark +structure.editor.markAsFalseDuplicatePerson = Mark structure.editor.markAsFalseDuplicateOf = as no duplicate of structure.editor.markAsFalseDuplicateQMark = ? structure.editor.merge = Merge title data of this duplicate +structure.editor.mergePerson = Merge personal data of this duplicate structure.editor.mergeInto = into this publication +structure.editor.mergeIntoPerson = into this person structure.editor.mergeQMark = ? structure.editor.move = Move structure.editor.moveHost = \ publication(s) linked to this host @@ -1056,6 +1070,7 @@ ubo.mailreport.subject = New entries in the ubo.mediaType = Media ubo.more = more ubo.nameIdentifier.orcid.validation = ORCIDs must match the pattern 0000-0000-0000-0000! +ubo.newPerson = Record new person: ubo.newPublication = Record new publication: ubo.newPublicationWizard = Register new publication ubo.newPublicationWizard.author.placeholder = Please only enter the family name of the first author @@ -1251,6 +1266,9 @@ user.profile.publications.num.one = one publication user.profile.publications.orcid.intro = Your ORCID\u00AE record with id {0} contains user.profile.publications.ubo.intro = The university bibliography contains user.profile.publications.ubo.outro = \ linked to this LSF ID +user.profile.publications.ubo.outro.modsperson = \ linked to this person +user.profile.publications.ubo.outro.plural.modsperson.true = \ linked to this person +user.profile.publications.ubo.outro.plural.modsperson.false = \ linked to this person user.profile.publications.ubo.published.extro.plural.false = is in state editorially checked. user.profile.publications.ubo.published.extro.plural.true = are in state editorially checked. user.profile.publications.ubo.published.intro = With actually diff --git a/ubo-common/src/main/resources/config/ubo-common/mycore.properties b/ubo-common/src/main/resources/config/ubo-common/mycore.properties index b2a79aab9..ebd2725d7 100644 --- a/ubo-common/src/main/resources/config/ubo-common/mycore.properties +++ b/ubo-common/src/main/resources/config/ubo-common/mycore.properties @@ -360,6 +360,7 @@ MCR.ContentTransformer.response.Stylesheet=xsl/response.xsl,%UBO.DefaultHTMLLayo MCR.ContentTransformer.response-statistics.Stylesheet=xsl/response-statistics.xsl,%UBO.DefaultHTMLLayoutSteps% MCR.ContentTransformer.response-duplicates.Stylesheet=xsl/response-duplicates.xsl,%UBO.DefaultHTMLLayoutSteps% MCR.ContentTransformer.basket-objects.Stylesheet=xsl/basket-bibentries.xsl,%UBO.DefaultHTMLLayoutSteps% +MCR.ContentTransformer.response-person.Stylesheet=xsl/response-person.xsl,%UBO.DefaultHTMLLayoutSteps% MCR.ContentTransformer.mycoreobject.Stylesheet=xsl/mycoreobject.xsl,%UBO.DefaultHTMLLayoutSteps% MCR.ContentTransformer.mycoreobject-structure.Stylesheet=xsl/mycoreobject-structure.xsl,%UBO.DefaultHTMLLayoutSteps% MCR.ContentTransformer.user.Stylesheet=xsl/user.xsl,%UBO.DefaultHTMLLayoutSteps% @@ -419,6 +420,7 @@ MCR.ContentTransformer.response-sidebar-admin.TransformerFactoryClass = org.apa MCR.ContentTransformer.response-sidebar-mini.TransformerFactoryClass = org.apache.xalan.processor.TransformerFactoryImpl MCR.ContentTransformer.response-statistics.TransformerFactoryClass = org.apache.xalan.processor.TransformerFactoryImpl MCR.ContentTransformer.response.TransformerFactoryClass = org.apache.xalan.processor.TransformerFactoryImpl +MCR.ContentTransformer.response-person.TransformerFactoryClass = org.apache.xalan.processor.TransformerFactoryImpl MCR.ContentTransformer.roles.TransformerFactoryClass = org.apache.xalan.processor.TransformerFactoryImpl MCR.ContentTransformer.site.TransformerFactoryClass = org.apache.xalan.processor.TransformerFactoryImpl MCR.ContentTransformer.user.TransformerFactoryClass = org.apache.xalan.processor.TransformerFactoryImpl @@ -657,7 +659,8 @@ MCR.URIResolver.ModuleResolver.ires=org.mycore.ubo.picker.IdentityResolver # TODO: make it all the same package or make it configurable or smth MCR.ContentTransformer.localpidsearch.Stylesheet=xsl/localpidsearch.xsl,%UBO.DefaultHTMLLayoutSteps% -MCR.IdentityPicker.strategy=org.mycore.ubo.local.Local +MCR.IdentityPicker.strategy=org.mycore.ubo.modsperson.MODSPersonSearch +#MCR.IdentityPicker.strategy=org.mycore.ubo.local.Local #MCR.IdentityPicker.strategy=org.mycore.ubo.ldap.picker.LDAP #MCR.IdentityPicker.strategy=org.mycore.ubo.ldap.picker.LDAPWithLocal diff --git a/ubo-common/src/main/resources/config/ubo-common/solr/main/solr-schema.json b/ubo-common/src/main/resources/config/ubo-common/solr/main/solr-schema.json index d2f0ab5a9..7c4b89990 100644 --- a/ubo-common/src/main/resources/config/ubo-common/solr/main/solr-schema.json +++ b/ubo-common/src/main/resources/config/ubo-common/solr/main/solr-schema.json @@ -519,6 +519,24 @@ "stored": true } }, + { + "add-field": { + "name": "ref_person", + "type": "ubo_id", + "multiValued": true, + "indexed": true, + "stored": true + } + }, + { + "add-field": { + "name": "alternative_name", + "type": "ubo_string_ci", + "multiValued": true, + "indexed": true, + "stored": true + } + }, { "add-field": { "name": "place", diff --git a/ubo-common/src/main/resources/xsl/mods-display.xsl b/ubo-common/src/main/resources/xsl/mods-display.xsl index 57a86d34b..e10a8424b 100644 --- a/ubo-common/src/main/resources/xsl/mods-display.xsl +++ b/ubo-common/src/main/resources/xsl/mods-display.xsl @@ -410,6 +410,18 @@ + + + + plain + + + - + + + + + plain @@ -557,7 +569,7 @@
- + @@ -617,11 +629,13 @@
- - + + + + - + @@ -670,44 +684,41 @@
-
- - - / - - + + + - - - - - + + + +
+ - + - - - + + + ORCID iD - - + + - +
@@ -716,6 +727,11 @@
+ + + / + + @@ -728,26 +744,9 @@
- - - - - - - ORCID iD - - - - - - - - - - - - - + + +
@@ -770,6 +769,31 @@
+ + + + + + + + + + + ORCID iD + + + + + + + + + + + + + + @@ -1208,6 +1232,13 @@ + + + + + + + @@ -1779,6 +1810,7 @@ ) + diff --git a/ubo-common/src/main/resources/xsl/mycoreobject.xsl b/ubo-common/src/main/resources/xsl/mycoreobject.xsl index ac694641a..1b6e1dae9 100644 --- a/ubo-common/src/main/resources/xsl/mycoreobject.xsl +++ b/ubo-common/src/main/resources/xsl/mycoreobject.xsl @@ -23,6 +23,8 @@ + + @@ -38,6 +40,11 @@ + <xsl:apply-templates select="/mycoreobject" mode="pageTitle" /> + + + + @@ -46,7 +53,6 @@ - @@ -58,12 +64,14 @@ - - - - - + + diff --git a/ubo-common/src/main/vue/dedup/src/router/index.ts b/ubo-common/src/main/vue/dedup/src/router/index.ts index f669cc5b3..c66e09630 100644 --- a/ubo-common/src/main/vue/dedup/src/router/index.ts +++ b/ubo-common/src/main/vue/dedup/src/router/index.ts @@ -2,6 +2,7 @@ import { createRouter, createWebHistory } from 'vue-router' import DedupListView from '../views/DedupListView.vue' import {getWebApplicationBaseURL} from "@/api/Utils.ts"; import NoDuplicateListView from "@/views/NoDuplicateListView.vue"; +import DedupPersonListView from "@/views/DedupPersonListView.vue"; export function getContext(): string { if (import.meta.env.DEV) { @@ -24,6 +25,11 @@ const router = createRouter({ path: '/no-duplicates', name: 'noDuplicates', component: NoDuplicateListView + }, + { + path: '/person-duplicates', + name: 'personDuplicates', + component: DedupPersonListView } ], }) diff --git a/ubo-common/src/main/vue/dedup/src/views/DedupPersonListView.vue b/ubo-common/src/main/vue/dedup/src/views/DedupPersonListView.vue new file mode 100644 index 000000000..1da8d0152 --- /dev/null +++ b/ubo-common/src/main/vue/dedup/src/views/DedupPersonListView.vue @@ -0,0 +1,171 @@ + + + + + + diff --git a/ubo-common/src/main/vue/dedup/src/views/NoDuplicateListView.vue b/ubo-common/src/main/vue/dedup/src/views/NoDuplicateListView.vue index ec90e7170..93e5e2a13 100644 --- a/ubo-common/src/main/vue/dedup/src/views/NoDuplicateListView.vue +++ b/ubo-common/src/main/vue/dedup/src/views/NoDuplicateListView.vue @@ -44,10 +44,12 @@ - + + - + + {{ 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: + *
    + *
  1. has a connection id
  2. + *
  3. local realm
  4. + *
  5. has not logged in yet
  6. + *
+ * @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! +

+
+
+ +
+
+ + + + + + + + + + + + + + + + + + + + +
+ + + + + +
+ + +
+
+ + +
+ + +
+
+ + +
+ + + + + + insert remove +
+
+ + +
+ +
+ + + insert remove +
+
+ + +
+ + +
+ +
+ +
+ +
+ + +
+ + + insert remove +
+
+ +
+ +
+ + + + +
+ +
+
+
+
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 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + <xsl:value-of select="$title" /> + <xsl:value-of select="i18n:translate('structure.editor.edit')"/> + + + + +
+ +
+ + + +
+ + + + + + + + + person + + + + base + + + + + + + + + + + + + + + + + + + + + duplicate + + + + + + + + + + + + + +
+ + + +
+ + col-11 ubo-structure__content + + ubo-structure__content--info + ubo-structure__content--warning + ubo-structure__content--default + + + + + + + + + + + + + + +
+
+ +
+ + + + + + +
+ +
+
+
+
+ + + + + + + + + + Evtl. Dublette + + + + + + + + + + + + +
+ + divs + +
+
+ + +
+ +
+
+ + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
+ + + + + + + + + + + + +