diff --git a/api/src/main/java/edu/cornell/mannlib/vitro/webapp/controller/individual/VIVOIndividualResponseBuilderExtension.java b/api/src/main/java/edu/cornell/mannlib/vitro/webapp/controller/individual/VIVOIndividualResponseBuilderExtension.java index da71d86d..3615c266 100644 --- a/api/src/main/java/edu/cornell/mannlib/vitro/webapp/controller/individual/VIVOIndividualResponseBuilderExtension.java +++ b/api/src/main/java/edu/cornell/mannlib/vitro/webapp/controller/individual/VIVOIndividualResponseBuilderExtension.java @@ -2,6 +2,7 @@ package edu.cornell.mannlib.vitro.webapp.controller.individual; import edu.cornell.mannlib.vitro.webapp.config.ConfigurationProperties; import edu.cornell.mannlib.vitro.webapp.controller.VitroRequest; +import org.vivoweb.webapp.controller.freemarker.CreateAndLinkResourceController; import javax.servlet.ServletContextEvent; import javax.servlet.ServletContextListener; @@ -24,6 +25,13 @@ public class VIVOIndividualResponseBuilderExtension implements IndividualRespons public void addOptions(VitroRequest vreq, Map body) { addAltMetricOptions(vreq, body); addPlumPrintOptions(vreq, body); + addEnabledClaimingSources(vreq, body); + } + + private void addEnabledClaimingSources(VitroRequest vreq, Map body) { + ConfigurationProperties props = ConfigurationProperties.getBean(vreq); + body.put("claimSources", CreateAndLinkResourceController.getEnabledProviders(props)); + } private void addAltMetricOptions(VitroRequest vreq, Map body) { diff --git a/api/src/main/java/org/vivoweb/webapp/controller/freemarker/CreateAndLinkResourceController.java b/api/src/main/java/org/vivoweb/webapp/controller/freemarker/CreateAndLinkResourceController.java new file mode 100644 index 00000000..e5314521 --- /dev/null +++ b/api/src/main/java/org/vivoweb/webapp/controller/freemarker/CreateAndLinkResourceController.java @@ -0,0 +1,1922 @@ +/* $This file is distributed under the terms of the license in /doc/license.txt$ */ + +package org.vivoweb.webapp.controller.freemarker; + +import edu.cornell.mannlib.vedit.beans.LoginStatusBean; +import edu.cornell.mannlib.vitro.webapp.auth.permissions.SimplePermission; +import edu.cornell.mannlib.vitro.webapp.auth.policy.PolicyHelper; +import edu.cornell.mannlib.vitro.webapp.auth.requestedAction.AuthorizationRequest; +import edu.cornell.mannlib.vitro.webapp.auth.requestedAction.propstmt.AddDataPropertyStatement; +import edu.cornell.mannlib.vitro.webapp.auth.requestedAction.propstmt.AddObjectPropertyStatement; +import edu.cornell.mannlib.vitro.webapp.beans.Individual; +import edu.cornell.mannlib.vitro.webapp.beans.SelfEditingConfiguration; +import edu.cornell.mannlib.vitro.webapp.beans.UserAccount; +import edu.cornell.mannlib.vitro.webapp.config.ConfigurationProperties; +import edu.cornell.mannlib.vitro.webapp.controller.VitroRequest; +import edu.cornell.mannlib.vitro.webapp.controller.freemarker.FreemarkerHttpServlet; +import edu.cornell.mannlib.vitro.webapp.controller.freemarker.responsevalues.ResponseValues; +import edu.cornell.mannlib.vitro.webapp.controller.freemarker.responsevalues.TemplateResponseValues; +import edu.cornell.mannlib.vitro.webapp.dao.IndividualDao; +import edu.cornell.mannlib.vitro.webapp.dao.InsertException; +import edu.cornell.mannlib.vitro.webapp.dao.NewURIMakerVitro; +import edu.cornell.mannlib.vitro.webapp.modelaccess.ModelNames; +import edu.cornell.mannlib.vitro.webapp.rdfservice.ChangeSet; +import edu.cornell.mannlib.vitro.webapp.rdfservice.RDFService; +import edu.cornell.mannlib.vitro.webapp.rdfservice.RDFServiceException; +import edu.cornell.mannlib.vitro.webapp.rdfservice.ResultSetConsumer; +import org.apache.commons.lang3.ArrayUtils; +import org.apache.commons.lang3.StringUtils; +import org.apache.jena.query.QuerySolution; +import org.apache.jena.rdf.model.Literal; +import org.apache.jena.rdf.model.Model; +import org.apache.jena.rdf.model.ModelFactory; +import org.apache.jena.rdf.model.Property; +import org.apache.jena.rdf.model.RDFNode; +import org.apache.jena.rdf.model.ResIterator; +import org.apache.jena.rdf.model.Resource; +import org.apache.jena.rdf.model.Statement; +import org.apache.jena.rdf.model.StmtIterator; +import org.apache.jena.vocabulary.RDF; +import org.apache.jena.vocabulary.RDFS; +import org.vivoweb.webapp.createandlink.Citation; +import org.vivoweb.webapp.createandlink.CreateAndLinkResourceProvider; +import org.vivoweb.webapp.createandlink.CreateAndLinkUtils; +import org.vivoweb.webapp.createandlink.ExternalIdentifiers; +import org.vivoweb.webapp.createandlink.ResourceModel; +import org.vivoweb.webapp.createandlink.crossref.CrossrefCreateAndLinkResourceProvider; +import org.vivoweb.webapp.createandlink.pubmed.PubMedCreateAndLinkResourceProvider; + +import javax.servlet.ServletConfig; +import javax.servlet.ServletContext; +import javax.servlet.ServletException; +import javax.servlet.annotation.WebServlet; +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.net.MalformedURLException; +import java.net.URL; +import java.util.ArrayList; +import java.util.Comparator; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; + +import static edu.cornell.mannlib.vitro.webapp.auth.requestedAction.RequestedAction.SOME_LITERAL; +import static edu.cornell.mannlib.vitro.webapp.auth.requestedAction.RequestedAction.SOME_PREDICATE; +import static edu.cornell.mannlib.vitro.webapp.auth.requestedAction.RequestedAction.SOME_URI; + +/** + * Main controller class for claiming (creating and/or linking) resources to a profile + */ +@WebServlet(name = "CreateAndLinkResource", urlPatterns = {"/createAndLink/*"} ) +public class CreateAndLinkResourceController extends FreemarkerHttpServlet { + // Must be able to edit your own account to claim publications + public static final AuthorizationRequest REQUIRED_ACTIONS = SimplePermission.EDIT_OWN_ACCOUNT.ACTION; + + // Mappings for publication type to ontology types / classes + private static final Map typeToClassMap = new HashMap<>(); + + // Providers for resolving ids in different resource providers (e.g. CrossRef, PubMed) + private static final Map providers = new HashMap<>(); + + private static boolean providersRegistered = false; + + /** + * URIs of types and predicates in the VIVO ontology that we need for creating resources + */ + public static final String BIBO_ABSTRACT = "http://purl.org/ontology/bibo/abstract"; + public static final String BIBO_ARTICLE = "http://purl.org/ontology/bibo/Article"; + public static final String BIBO_BOOK = "http://purl.org/ontology/bibo/Book"; + public static final String BIBO_DOI = "http://purl.org/ontology/bibo/doi"; + public static final String BIBO_ISBN10 = "http://purl.org/ontology/bibo/isbn10"; + public static final String BIBO_ISBN13 = "http://purl.org/ontology/bibo/isbn13"; + public static final String BIBO_ISSN = "http://purl.org/ontology/bibo/issn"; + public static final String BIBO_ISSUE = "http://purl.org/ontology/bibo/issue"; + public static final String BIBO_JOURNAL = "http://purl.org/ontology/bibo/Journal"; + public static final String BIBO_PAGE_COUNT = "http://purl.org/ontology/bibo/numPages"; + public static final String BIBO_PAGE_END = "http://purl.org/ontology/bibo/pageEnd"; + public static final String BIBO_PAGE_START = "http://purl.org/ontology/bibo/pageStart"; + public static final String BIBO_PMID = "http://purl.org/ontology/bibo/pmid"; + public static final String BIBO_VOLUME = "http://purl.org/ontology/bibo/volume"; + + public static final String FOAF_FIRSTNAME = "http://xmlns.com/foaf/0.1/firstName"; + public static final String FOAF_LASTNAME = "http://xmlns.com/foaf/0.1/lastName"; + + public static final String OBO_CONTACT_INFO_FOR = "http://purl.obolibrary.org/obo/ARG_2000029"; + public static final String OBO_HAS_CONTACT_INFO = "http://purl.obolibrary.org/obo/ARG_2000028"; + + public static final String OBO_INHERES_IN = "http://purl.obolibrary.org/obo/RO_0000052"; + public static final String OBO_BEARER_OF = "http://purl.obolibrary.org/obo/RO_0000053"; + + public static final String RDFS_LABEL = "http://www.w3.org/2000/01/rdf-schema#label"; + + public static final String VIVO_AUTHORSHIP = "http://vivoweb.org/ontology/core#Authorship"; + public static final String VIVO_DATETIME = "http://vivoweb.org/ontology/core#dateTime"; + public static final String VIVO_DATETIMEPRECISION = "http://vivoweb.org/ontology/core#dateTimePrecision"; + public static final String VIVO_DATETIMEVALUE = "http://vivoweb.org/ontology/core#dateTimeValue"; + public static final String VIVO_EDITORSHIP = "http://vivoweb.org/ontology/core#Editorship"; + public static final String VIVO_HASPUBLICATIONVENUE = "http://vivoweb.org/ontology/core#hasPublicationVenue"; + public static final String VIVO_PMCID = "http://vivoweb.org/ontology/core#pmcid"; + public static final String VIVO_PUBLICATIONVENUEFOR = "http://vivoweb.org/ontology/core#publicationVenueFor"; + public static final String VIVO_PUBLISHER = "http://vivoweb.org/ontology/core#publisher"; + public static final String VIVO_PUBLISHER_CLASS = "http://vivoweb.org/ontology/core#Publisher"; + public static final String VIVO_PUBLISHER_OF = "http://vivoweb.org/ontology/core#publisherOf"; + public static final String VIVO_RANK = "http://vivoweb.org/ontology/core#rank"; + public static final String VIVO_RELATEDBY = "http://vivoweb.org/ontology/core#relatedBy"; + public static final String VIVO_RELATES = "http://vivoweb.org/ontology/core#relates"; + + public static final String VCARD_FAMILYNAME = "http://www.w3.org/2006/vcard/ns#familyName"; + public static final String VCARD_GIVENNAME = "http://www.w3.org/2006/vcard/ns#givenName"; + public static final String VCARD_HAS_NAME = "http://www.w3.org/2006/vcard/ns#hasName"; + public static final String VCARD_HAS_URL = "http://www.w3.org/2006/vcard/ns#hasURL"; + public static final String VCARD_INDIVIDUAL = "http://www.w3.org/2006/vcard/ns#Individual"; + public static final String VCARD_KIND = "http://www.w3.org/2006/vcard/ns#Kind"; + public static final String VCARD_NAME = "http://www.w3.org/2006/vcard/ns#Name"; + public static final String VCARD_URL_CLASS = "http://www.w3.org/2006/vcard/ns#URL"; + public static final String VCARD_URL_PROPERTY = "http://www.w3.org/2006/vcard/ns#url"; + + private static final String PROVIDER_DOI = "doi"; + private static final String PROVIDER_PMID = "pmid"; + + @Override + public void init(ServletConfig config) throws ServletException { + super.init(config); + + ServletContext ctx = config.getServletContext(); + ConfigurationProperties props = ConfigurationProperties.getBean(ctx); + + // One-off initialization of class state + // Add recognized publication types to the type map, along with the corresponding ontology URI + + typeToClassMap.put("article", "http://purl.org/ontology/bibo/Article"); + typeToClassMap.put("article-journal", "http://purl.org/ontology/bibo/AcademicArticle"); + typeToClassMap.put("book", "http://purl.org/ontology/bibo/Book"); + typeToClassMap.put("chapter", "http://purl.org/ontology/bibo/Chapter"); + typeToClassMap.put("dataset", "http://vivoweb.org/ontology/core#Dataset"); + typeToClassMap.put("figure", "http://purl.org/ontology/bibo/Image"); + typeToClassMap.put("graphic", "http://purl.org/ontology/bibo/Image"); + typeToClassMap.put("legal_case", "http://purl.org/ontology/bibo/LegalCaseDocument"); + typeToClassMap.put("legislation", "http://purl.org/ontology/bibo/Legislation"); + typeToClassMap.put("manuscript", "http://purl.org/ontology/bibo/Manuscript"); + typeToClassMap.put("map", "http://purl.org/ontology/bibo/Map"); + typeToClassMap.put("musical_score", "http://vivoweb.org/ontology/core#Score"); + typeToClassMap.put("paper-conference", "http://vivoweb.org/ontology/core#ConferencePaper"); + typeToClassMap.put("patent", "http://purl.org/ontology/bibo/Patent"); + typeToClassMap.put("personal_communication", "http://purl.org/ontology/bibo/PersonalCommunicationDocument"); + typeToClassMap.put("post-weblog", "http://vivoweb.org/ontology/core#BlogPosting"); + typeToClassMap.put("report", "http://purl.org/ontology/bibo/Report"); + typeToClassMap.put("review", "http://vivoweb.org/ontology/core#Review"); + typeToClassMap.put("speech", "http://vivoweb.org/ontology/core#Speech"); + typeToClassMap.put("thesis", "http://purl.org/ontology/bibo/Thesis"); + typeToClassMap.put("webpage", "http://purl.org/ontology/bibo/Webpage"); + + // Populate the registry of resource providers + registerProviders(props); + } + + public static Set getEnabledProviders(ConfigurationProperties props) { + if (!providersRegistered) { + registerProviders(props); + } + + return providers.keySet(); + } + + public static synchronized void registerProviders(ConfigurationProperties props) { + if (!providersRegistered) { + String provStr = props.getProperty("createAndLink.providers", "doi, pmid"); + if (!StringUtils.isEmpty(provStr)) { + String[] provArr = provStr.split("[,]"); + for (String provId : provArr) { + if (PROVIDER_DOI.equalsIgnoreCase(provId.trim())) { + providers.put(PROVIDER_DOI, new CrossrefCreateAndLinkResourceProvider()); + } else if (PROVIDER_PMID.equalsIgnoreCase(provId.trim())) { + providers.put(PROVIDER_PMID, new PubMedCreateAndLinkResourceProvider()); + } + } + } + + providersRegistered = true; + } + } + + /** + * Ensure that we can only be called if the user has the correct permissions + * + * @param vreq + * @return + */ + @Override + protected AuthorizationRequest requiredActions(VitroRequest vreq) { + return REQUIRED_ACTIONS; + } + + /** + * Main method for the resource claiming (create and link) workflow + * + * @param vreq + * @return + */ + @Override + protected ResponseValues processRequest(VitroRequest vreq) { + // Get the current URL for parsing + String requestURI = vreq.getRequestURI(); + + CreateAndLinkResourceProvider provider = null; + + // First part of URL path after /createAndLink/ is used to identify the resource type (DOI, PubMed ID, etc) + String externalProvider = null; + int typePos = requestURI.indexOf("/createAndLink/") + 15; + if (typePos < requestURI.length()) { + if (requestURI.indexOf('/', typePos) > typePos) { + externalProvider = requestURI.substring(typePos, requestURI.indexOf('/', typePos) - 1); + } else { + externalProvider = requestURI.substring(typePos); + } + + // Normalize the resource type key, and get the appropriate provider + externalProvider = externalProvider.trim().toLowerCase(); + if (providers.containsKey(externalProvider)) { + provider = providers.get(externalProvider); + } + } + + // If no provider was found (invalid path), return an error to the user + if (provider == null) { + return new TemplateResponseValues("unknownResourceType.ftl"); + } + + // Obtain the DAO for getting an individual (that represents a person profile) + IndividualDao individualDao = vreq.getWebappDaoFactory().getIndividualDao(); + Individual person = null; + + // If a person profile URI has been passed as a paremeter, ensure that the individual exists + String profileUri = vreq.getParameter("profileUri"); + if (!StringUtils.isEmpty(profileUri)) { + person = individualDao.getIndividualByURI(profileUri); + } + + boolean isProfileUriForLoggedIn = false; + + // Get the currently logged in user + UserAccount loggedInAccount = LoginStatusBean.getCurrentUser(vreq); + SelfEditingConfiguration sec = SelfEditingConfiguration.getBean(vreq); + + // Find the profile(s) associated with this user + List assocInds = sec.getAssociatedIndividuals(vreq.getWebappDaoFactory().getIndividualDao(), loggedInAccount.getExternalAuthId()); + if (!assocInds.isEmpty()) { + if (person == null) { + // If we have associated profiles, ensure that a valid person profile really does exist + profileUri = assocInds.get(0).getURI(); + if (!StringUtils.isEmpty(profileUri)) { + person = individualDao.getIndividualByURI(profileUri); + isProfileUriForLoggedIn = true; + } + } else if (!StringUtils.isEmpty(profileUri)){ + for (Individual ind : assocInds) { + if (ind.getURI().equalsIgnoreCase(profileUri)) { + isProfileUriForLoggedIn = true; + } + } + } + } + + // If we still haven't got a person, return an error to the user + if (person == null) { + return new TemplateResponseValues("unknownProfile.ftl"); + } + + // If the profile isn't associated with the logged in user + if (!isProfileUriForLoggedIn) { + // Check that we have back end editing priveleges + if (!PolicyHelper.isAuthorizedForActions(vreq, SimplePermission.DO_BACK_END_EDITING.ACTION)) { + // If all else fails, can we add statements to this individual? + AddDataPropertyStatement adps = new AddDataPropertyStatement(vreq.getJenaOntModel(), profileUri, SOME_URI, SOME_LITERAL); + AddObjectPropertyStatement aops = new AddObjectPropertyStatement(vreq.getJenaOntModel(), profileUri, SOME_PREDICATE, SOME_URI); + if (!PolicyHelper.isAuthorizedForActions(vreq, adps.or(aops))) { + return new TemplateResponseValues("unauthorizedForProfile.ftl"); + } + } + } + + // Create a map of common values to pass to the templates + Map templateValues = new HashMap<>(); + templateValues.put("link", profileUri); + templateValues.put("label", provider.getLabel()); + templateValues.put("provider", externalProvider); + templateValues.put("profileUri", profileUri); + templateValues.put("personLabel", person.getRdfsLabel()); + templateValues.put("personThumbUrl", person.getThumbUrl()); + + // Get the requested action (e.g. find, confirm) + String action = vreq.getParameter("action"); + if (action == null) { + action = ""; + } + + String externalIdsToFind = null; + + // If the user has pressed a "confirm" button + if ("confirmID".equals(action)) { + // Get all of the external IDs represented on the page + String[] externalIds = vreq.getParameterValues("externalId"); + + // Check that we have IDs to process + if (!ArrayUtils.isEmpty(externalIds)) { + // Create a holder for statements already in the triple store, and another for the changes + Model existingModel = ModelFactory.createDefaultModel(); + Model updatedModel = ModelFactory.createDefaultModel(); + + // Loop through each external ID that was on the page + for (String externalId : externalIds) { + // Get the normalized ID from the resource provider + externalId = provider.normalize(externalId); + + // Ensure that we have an ID + if (!StringUtils.isEmpty(externalId)) { + // Check that the user is claiming a relationship to the resource + if (!"notmine".equalsIgnoreCase(vreq.getParameter("contributor" + externalId))) { + // If we are processing a resource that is already in VIVO, get the Vivo URI from the form + String vivoUri = vreq.getParameter("vivoUri" + externalId); + + // If we don't already know that the resource has been created in VIVO + if (StringUtils.isEmpty(vivoUri)) { + // Check that it hasn't been created since when we first rendered the page + ExternalIdentifiers allExternalIds = provider.allExternalIDsForFind(externalId); + vivoUri = findInVIVO(vreq, allExternalIds, profileUri, null); + } + + // If we haven't got an existing VIVO resource by this point, create it + if (StringUtils.isEmpty(vivoUri)) { + ResourceModel resourceModel = null; + + // Get the publication type from the form + String typeUri = vreq.getParameter("type" + externalId); + + // Get the appropriate resource provider for the external ID from the form + String resourceProvider = vreq.getParameter("externalProvider" + externalId); + + // Get an intermediate ResourceModel from the provider + if (providers.containsKey(resourceProvider)) { + resourceModel = providers.get(resourceProvider).makeResourceModel(externalId, vreq.getParameter("externalResource" + externalId)); + } else { + resourceModel = provider.makeResourceModel(externalId, vreq.getParameter("externalResource" + externalId)); + } + + // If we have an intermediate model, create the VIVO representation from the model + if (resourceModel != null) { + vivoUri = createVIVOObject(vreq, updatedModel, resourceModel, typeUri); + } + } else { + // Get the existing statements for the model, and add them to the both in-memory models + Model existingResourceModel = getExistingResource(vreq, vivoUri); + existingModel.add(existingResourceModel); + updatedModel.add(existingResourceModel); + } + + // Process the user's chosen relationship with the resource, updating the updated model + processRelationships(vreq, updatedModel, vivoUri, profileUri, vreq.getParameter("contributor" + externalId)); + } + } + } + + // Finished processing confirmation, write the differences between the existing and updated model + writeChanges(vreq.getRDFService(), existingModel, updatedModel); + } + + // Get any IDs that have not yet been processed from the form + externalIdsToFind = vreq.getParameter("remainderIds"); + + // If There are no IDs left to process, go back to the entry screen + if (StringUtils.isEmpty(externalIdsToFind)) { + templateValues.put("showConfirmation", true); + return new TemplateResponseValues("createAndLinkResourceEnterID.ftl", templateValues); + } + } else if ("findID".equals(action)) { + // User has pressed a "findID" button - e.g. has entered IDs of resources on the initial entry screen + externalIdsToFind = vreq.getParameter("externalIds"); + } + + // If we have external IDs to find (either directly from the entry form, or unprocessed from a long list) + if (!StringUtils.isEmpty(externalIdsToFind)) { + Set uniqueIds = new HashSet<>(); + Set remainderIds = new HashSet<>(); + List citations = new ArrayList<>(); + + // Split the passed IDs into a parseable array (separated by whitespace, oe comma) + String[] externalIdArr = externalIdsToFind.split("[\\s,]+"); + + // Go through each identifier + for (String externalId : externalIdArr) { + // Normalize the identifier, and create a set of unique identifiers (remove duplicates) + externalId = provider.normalize(externalId); + if (!StringUtils.isEmpty(externalId) && !uniqueIds.contains(externalId)) { + uniqueIds.add(externalId); + } + } + + int idCount = 0; + // Loop through all the unique identifiers + for (String externalId : uniqueIds) { + // If we've already processed 5 or more identifiers + if (idCount > 4) { + // Add the identifier to the remainder list to be processed on the next page + remainderIds.add(externalId); + } else { + // Prepare a citation object for this identifier + Citation citation = new Citation(); + citation.externalId = externalId; + + // First, resolve all known identifiers for the identifier processed + ExternalIdentifiers allExternalIds = provider.allExternalIDsForFind(externalId); + + // Try to find an existing resource that has one of the known external identifiers + // Note, this will populate the citation object if it exists + citation.vivoUri = findInVIVO(vreq, allExternalIds, profileUri, citation); + + // If we did not find a resource in VIVO + if (StringUtils.isEmpty(citation.vivoUri)) { + // If we have a DOI for the resource, first attempt to find the metadata via DOI + if (!StringUtils.isEmpty(allExternalIds.DOI)) { + CreateAndLinkResourceProvider doiProvider = providers.get("doi"); + if (doiProvider != null) { + // Attempt to find the DOI in via the doi resource provider (fills the citation object) + citation.externalResource = doiProvider.findInExternal(allExternalIds.DOI, citation); + + // If we were successful, record that the record was looked up via DOI + if (!StringUtils.isEmpty(citation.externalResource)) { + citation.externalProvider = "doi"; + } + } + } + + // Did not resolve the resource via DOI, so look in the provider for the original identifier + if (StringUtils.isEmpty(citation.externalResource)) { + // Only if the original identifier was not a DOI + if (!"doi".equalsIgnoreCase(externalProvider)) { + citation.externalResource = provider.findInExternal(externalId, citation); + citation.externalProvider = externalProvider; + } + } + } + + // Guess which author in the available metadata is the user claiming the work + proposeAuthorToLink(vreq, citation, profileUri); + + // Conver the type in the citation to a VIVO type uri for use in the confirmation form + citation.typeUri = typeToClassMap.getOrDefault(citation.type, BIBO_ARTICLE); + + // If we have found a citation, add it to the list of citations to display + if (citation.vivoUri != null || citation.externalResource != null) { + citations.add(citation); + + // Increment the count of processed identifiers + idCount++; + } else { + citation.showError = true; + citations.add(citation); + } + } + } + + // If we have found records to claim + if (citations.size() > 0) { + // Add the citations to the values to pass to the template + templateValues.put("citations", citations); + + // Add the list of known publication types + templateValues.put("publicationTypes", getPublicationTypes(vreq)); + + // If there are IDs still left to process, add them to the values passed to the template + if (remainderIds.size() > 0) { + templateValues.put("remainderIds", StringUtils.join(remainderIds, "\n")); + templateValues.put("remainderCount", remainderIds.size()); + } + + // Show the confirmation page for the processed identifiers + return new TemplateResponseValues("createAndLinkResourceConfirm.ftl", templateValues); + } else { + // Nothing to show, so go back to the form, passing an indicator that nothing was found + templateValues.put("notfound", true); + return new TemplateResponseValues("createAndLinkResourceEnterID.ftl", templateValues); + } + } + + // Show the entry form for a user to enter a set of identifiers + return new TemplateResponseValues("createAndLinkResourceEnterID.ftl", templateValues); + } + + private String getFormattedProfileName(VitroRequest vreq, String profileUri) { + final Citation.Name name = new Citation.Name(); + + name.name = null; + + String vcardQuery = "SELECT ?givenName ?familyName\n" + + "WHERE\n" + + "{\n" + + " <" + profileUri + "> <" + OBO_HAS_CONTACT_INFO + "> ?vCard .\n" + + " ?vCard <" + VCARD_HAS_NAME + "> ?vCardName .\n" + + " ?vCardName <" + VCARD_FAMILYNAME + "> ?familyName .\n" + + " OPTIONAL { ?vCardName <" + VCARD_GIVENNAME + "> ?givenName . }\n" + + "}\n"; + + try { + // Process the query + vreq.getRDFService().sparqlSelectQuery(vcardQuery, new ResultSetConsumer() { + @Override + protected void processQuerySolution(QuerySolution qs) { + // Get the name(s) from the result set + Literal familyName = qs.contains("familyName") ? qs.getLiteral("familyName") : null; + Literal givenName = qs.contains("givenName") ? qs.getLiteral("givenName") : null; + + if (StringUtils.isEmpty(name.name)) { + // If we have a first / last name, create a formatted author string + if (familyName != null) { + if (givenName != null) { + name.name = CreateAndLinkUtils.formatAuthorString(familyName.getString(), givenName.getString()); + } else { + name.name = CreateAndLinkUtils.formatAuthorString(familyName.getString(), null); + } + } + } + } + }); + } catch (RDFServiceException e) { + e.printStackTrace(); + } + + String foafQuery = "SELECT ?givenName ?familyName\n" + + "WHERE\n" + + "{\n" + + " <" + profileUri + "> <" + FOAF_LASTNAME + "> ?familyName .\n" + + " OPTIONAL { <" + profileUri + "> <" + FOAF_FIRSTNAME + "> ?givenName . }\n" + + "}"; + + if (StringUtils.isEmpty(name.name)) { + try { + // Process the query + vreq.getRDFService().sparqlSelectQuery(foafQuery, new ResultSetConsumer() { + @Override + protected void processQuerySolution(QuerySolution qs) { + // Get the name(s) from the result set + Literal familyName = qs.contains("familyName") ? qs.getLiteral("familyName") : null; + Literal givenName = qs.contains("givenName") ? qs.getLiteral("givenName") : null; + + if (StringUtils.isEmpty(name.name)) { + // If we have a first / last name, create a formatted author string + if (familyName != null) { + if (givenName != null) { + name.name = CreateAndLinkUtils.formatAuthorString(familyName.getString(), givenName.getString()); + } else { + name.name = CreateAndLinkUtils.formatAuthorString(familyName.getString(), null); + } + } + } + } + }); + } catch (RDFServiceException e) { + e.printStackTrace(); + } + } + + String labelQuery = "SELECT ?label\n" + + "WHERE\n" + + "{\n" + + " <" + profileUri + "> <" + RDFS_LABEL + "> ?label .\n" + + "}\n"; + + if (StringUtils.isEmpty(name.name)) { + try { + // Process the query + vreq.getRDFService().sparqlSelectQuery(labelQuery, new ResultSetConsumer() { + @Override + protected void processQuerySolution(QuerySolution qs) { + // Get the name(s) from the result set + Literal label = qs.contains("label") ? qs.getLiteral("label") : null; + + String authorStr = null; + if (label != null) { + // If we have a formatted label, normalize it to last name, initials + authorStr = label.getString(); + if (authorStr.indexOf(',') > -1) { + int endIdx = authorStr.indexOf(','); + while (endIdx < authorStr.length()) { + if (Character.isAlphabetic(authorStr.charAt(endIdx))) { + break; + } + endIdx++; + } + + if (endIdx < authorStr.length()) { + authorStr = authorStr.substring(0, endIdx + 1); + } else { + authorStr = authorStr.substring(0, authorStr.indexOf(',')); + } + } + } + + // If we have a formatted author string + if (authorStr != null) { + name.name = authorStr; + } + + } + }); + } catch (RDFServiceException e) { + e.printStackTrace(); + } + } + + return name.name; + } + + /** + * Method to find an author to propose for linking + * + * @param vreq + * @param citation + * @param profileUri + */ + protected void proposeAuthorToLink(VitroRequest vreq, final Citation citation, String profileUri) { + // If the resource has no idnetifiers, we have nothing to do + if (citation.authors == null) { + return; + } + + String authorStr = getFormattedProfileName(vreq, profileUri); + if (StringUtils.isEmpty(authorStr)) { + return; + } + + // If we have a formatted author string + String authorStrLwr = authorStr.toLowerCase(); + + // Find a match for the author string in the resource + for (Citation.Name author : citation.authors) { + if (author != null && author.name != null) { + String nameLwr = author.name.toLowerCase(); + if (nameLwr.startsWith(authorStrLwr) || authorStrLwr.startsWith(nameLwr)) { + author.proposed = true; + break; + } + } + } + } + + /** + * Find an existing resource in VIVO, and return a Model with the appropriate statements + * + * @param vreq + * @param uri + * @return + */ + protected Model getExistingResource(VitroRequest vreq, String uri) { + Model model = ModelFactory.createDefaultModel(); + + try { + String query = + "PREFIX vcard: \n" + + "PREFIX vivo: \n" + + "PREFIX obo: \n" + + "\n" + + "CONSTRUCT\n" + + "{\n" + + " <" + uri + "> ?pWork ?oWork .\n" + + " ?sJournal ?pJournal ?oJournal .\n" + + " ?sDateTime ?pDateTime ?oDateTime .\n" + + " ?sRel ?pRel ?oRel .\n" + + " ?sVCard a vcard:Individual .\n" + + " ?sVCard vcard:hasName ?sVCardName .\n" + + " ?sVCardName ?pVCardName ?oVCardName .\n" + + " ?sPerson ?pPerson ?oPerson .\n" + + "}\n" + + "WHERE\n" + + "{\n" + + " {\n" + + " <" + uri + "> ?pWork ?oWork .\n" + + " }\n" + + " UNION\n" + + " {\n" + + " <" + uri + "> vivo:hasPublicationVenue ?sJournal .\n" + + " ?sJournal ?pJournal ?oJournal .\n" + + " }\n" + + " UNION\n" + + " {\n" + + " <" + uri + "> vivo:dateTimeValue ?sDateTime .\n" + + " ?sDateTime ?pDateTime ?oDateTime .\n" + + " }\n" + + " UNION\n" + + " {\n" + + " <" + uri + "> vivo:relatedBy ?sRel .\n" + + " ?sRel ?pRel ?oRel .\n" + + " }\n" + + " UNION\n" + + " {\n" + + " <" + uri + "> vivo:relatedBy ?relationship .\n" + + " ?relationship ?pRel ?sPerson .\n" + + " ?sPerson ?pPerson ?oPerson .\n" + + " FILTER (?sPerson != <" + uri + ">)\n" + + " }\n" + + " UNION\n" + + " {\n" + + " <" + uri + "> vivo:relatedBy ?relationship .\n" + + " ?relationship ?pRel ?sPerson .\n" + + " ?sPerson obo:ARG_2000028 ?sVCard .\n" + + " ?sVCard vcard:hasName ?sVCardName .\n" + + " ?sVCardName ?pVCardName ?oVCardName .\n" + + " }\n" + + " UNION\n" + + " {\n" + + " <" + uri + "> vivo:relatedBy ?relationship .\n" + + " ?relationship ?pRel ?sVCard .\n" + + " ?sVCard vcard:hasName ?sVCardName .\n" + + " ?sVCardName ?pVCardName ?oVCardName .\n" + + " }\n" + + "}\n"; + + vreq.getRDFService().sparqlConstructQuery(query, model); + } catch (RDFServiceException e) { + } + + return model; + } + + /** + * Adjust the in-memory model to create the appropriate relationships for the claimed user role (authorship, editorship, etc) + * + * @param vreq + * @param model + * @param vivoUri + * @param userUri + * @param relationship + */ + protected void processRelationships(VitroRequest vreq, Model model, String vivoUri, String userUri, String relationship) { + if (relationship != null) { + // If authorship is being claimed + if (relationship.startsWith("author")) { + // Create an authorship context object + Resource authorship = model.createResource(getUnusedUri(vreq)); + authorship.addProperty(RDF.type, model.getResource(VIVO_AUTHORSHIP)); + + // Add the resource and the user as relates predicates of the context + authorship.addProperty(model.createProperty(VIVO_RELATES), model.getResource(vivoUri)); + authorship.addProperty(model.createProperty(VIVO_RELATES), model.getResource(userUri)); + + // Add related by predicates to the user and resource, linking to the context + model.getResource(vivoUri).addProperty(model.createProperty(VIVO_RELATEDBY), authorship); + model.getResource(userUri).addProperty(model.createProperty(VIVO_RELATEDBY), authorship); + + // If the relationship contains an author position + if (relationship.length() > 6) { + // Parse out the author position to a numeric rank + String posStr = relationship.substring(6); + int rank = Integer.parseInt(posStr, 10); + // Remove an existing authorship at that rank + removeAuthorship(vreq, model, vivoUri, rank); + try { + // Add the chosen rank to the authorship context created + authorship.addLiteral(model.createProperty(VIVO_RANK), rank); + } catch (NumberFormatException nfe) { + } + } + } else if (relationship.startsWith("editor")) { + // User is claiming editorship + Resource editorship = model.createResource(getUnusedUri(vreq)); + editorship.addProperty(RDF.type, model.getResource(VIVO_EDITORSHIP)); + + // Add the resource and the user as relates predicates of the context + editorship.addProperty(model.createProperty(VIVO_RELATES), model.getResource(vivoUri)); + editorship.addProperty(model.createProperty(VIVO_RELATES), model.getResource(userUri)); + + // Add related by predicates to the user and resource, linking to the context + model.getResource(vivoUri).addProperty(model.createProperty(VIVO_RELATEDBY), editorship); + model.getResource(userUri).addProperty(model.createProperty(VIVO_RELATEDBY), editorship); + } + } + } + + /** + * Removes an existing authorship at a given position, when that position is claimed by the author + * + * @param model + * @param rank + */ + protected void removeAuthorship(VitroRequest vreq, Model model, String vivoUri, int rank) { + // Prepare property / resources outsite of the loop + Property RANK_PREDICATE = model.getProperty(VIVO_RANK); + Property RELATES_PREDICATE = model.getProperty(VIVO_RELATES); + Resource AUTHORSHIP_RESOURCE = model.getResource(VIVO_AUTHORSHIP); + + // Lookp through all the subjects in the model + ResIterator iter = model.listSubjects(); + try { + while (iter.hasNext()) { + Resource subject = iter.next(); + // If the subject is an Authorship context + if (subject.hasProperty(RDF.type, AUTHORSHIP_RESOURCE)) { + // And the subject is related to the resource we are interested in + if (subject.hasProperty(RELATES_PREDICATE, model.getResource(vivoUri))) { + // And the subject is for the rank (position that we are interested in + if (subject.hasLiteral(RANK_PREDICATE, rank)) { + // Remove all the predicates referring to this authorship context + model.removeAll(null, null, subject); + + // List all the relates predicates on authorship context + StmtIterator stmtIterator = subject.listProperties(RELATES_PREDICATE); + try { + while (stmtIterator.hasNext()) { + Statement stmt = stmtIterator.next(); + RDFNode rdfNode = stmt.getObject(); + // Ensure the object of the statement is a resource + if (rdfNode.isResource()) { + Resource relatedResource = rdfNode.asResource(); + + // Ensure that the object resource is a VCARD + if (relatedResource.hasProperty(RDF.type, model.getResource(VCARD_INDIVIDUAL))) { + // If the VCARD has no other references + if (isVCardOnlyLinkedToAuthorship(vreq.getRDFService(), subject.getURI(), relatedResource.getURI())) { + // Remove the VCARD + removeVCard(model, relatedResource.getURI()); + } + } + } + } + } finally { + stmtIterator.close(); + } + // Remove all statements related to this authorship context + subject.removeProperties(); + } + } + } + } + } finally { + iter.close(); + } + } + + private boolean isVCardOnlyLinkedToAuthorship(final RDFService rdfService, final String authorshipUri, final String vcardUri) { + final List subjects = new ArrayList<>(); + String query = "SELECT ?s ?p\n" + + "WHERE\n" + + "{\n" + + " {\n" + + " \t?s ?p <" + vcardUri + "> .\n" + + " }\n" + + "}\n"; + + try { + rdfService.sparqlSelectQuery(query, new ResultSetConsumer() { + @Override + protected void processQuerySolution(QuerySolution qs) { + Resource subject = qs.getResource("s"); + if (!authorshipUri.equalsIgnoreCase(subject.getURI())) { + subjects.add(subject.getURI()); + } + } + }); + } catch (RDFServiceException e) { + } + + return subjects.size() == 0; + } + + /** + * Attempt to remove a VCARD from VIVO, ensuring that it is not being used first + * + * @param model + * @param vcardUri + */ + protected void removeVCard(Model model, String vcardUri) { + // Get the VCARD resource from the model + Resource vcard = model.getResource(vcardUri); + + // Remove the VCARD name + Statement vcardName = vcard.getProperty(model.createProperty(VCARD_HAS_NAME)); + if (vcardName != null) { + if (vcardName.getObject().isResource()) { + vcardName.getObject().asResource().removeProperties(); + } + } + + // Remove the VCARD + vcard.removeProperties(); + } + + /** + * Create a new resource in VIVO, based on the values of the intermediate model + * + * @param vreq + * @param model + * @param resourceModel + * @return + */ + protected String createVIVOObject(VitroRequest vreq, Model model, ResourceModel resourceModel, String typeUri) { + String vivoUri = getUnusedUri(vreq); + + // Create the resource in our model + Resource work = model.createResource(vivoUri); + + // Add the correct type to the resource + if (!StringUtils.isEmpty(typeUri)) { + // If the form passed a type uri, ensure that it is a known type URI + for (PublicationType publicationType: getPublicationTypes(vreq)) { + // We have a known type, so record it on the work + if (typeUri.equals(publicationType.getUri())) { + work.addProperty(RDF.type, model.getResource(typeUri)); + break; + } + } + } + + // If the work does not have a type set + if (!work.hasProperty(RDF.type)) { + // Try to map the type in the external model, or use academic article if none. + work.addProperty(RDF.type, model.getResource(typeToClassMap.getOrDefault(resourceModel.type, BIBO_ARTICLE))); + } + + // Add the title + if (!StringUtils.isEmpty(resourceModel.title)) { + work.addProperty(RDFS.label, resourceModel.title); + } + + // Add a DOI + if (!StringUtils.isEmpty(resourceModel.DOI)) { + String doi = new CrossrefCreateAndLinkResourceProvider().normalize(resourceModel.DOI); + work.addProperty(model.createProperty(BIBO_DOI), doi); + } + + // Add a PubMed ID + if (!StringUtils.isEmpty(resourceModel.PubMedID)) { + work.addProperty(model.createProperty(BIBO_PMID), resourceModel.PubMedID.toLowerCase()); + } + + // Add a PubMed Central ID + if (!StringUtils.isEmpty(resourceModel.PubMedCentralID)) { + work.addProperty(model.createProperty(VIVO_PMCID), resourceModel.PubMedCentralID.toLowerCase()); + } + + // Add the journal + if (!"book".equals(resourceModel.type) && !"chapter".equals(resourceModel.type)) { + if (resourceModel.ISSN != null && resourceModel.ISSN.length > 0) { + Resource journal = null; + + // Try to find the ISSN in VIVO + String journalUri = findVIVOUriForISSNs(vreq.getRDFService(), resourceModel.ISSN); + + if (!StringUtils.isEmpty(journalUri)) { + // If we jave a Journal URI, get the resource from the model + journal = model.getResource(journalUri); + } else { + // Create a new journal, using the ISSN for a Uri + journal = model.createResource(getUnusedUri(vreq)); + journal.addProperty(RDFS.label, resourceModel.containerTitle); + journal.addProperty(RDF.type, model.getResource(BIBO_JOURNAL)); + for (String issn : resourceModel.ISSN) { + journal.addProperty(model.getProperty(BIBO_ISSN), issn); + } + + if (!StringUtils.isEmpty(resourceModel.publisher)) { + String publisherUri = findVIVOUriForPublisher(vreq.getRDFService(), resourceModel.publisher); + + Resource publisher = null; + if (!StringUtils.isEmpty(publisherUri)) { + publisher = model.getResource(publisherUri); + } else { + publisher = model.createResource(getPublisherURI(vreq, resourceModel.publisher)); + publisher.addProperty(RDFS.label, resourceModel.publisher); + publisher.addProperty(RDF.type, model.getResource(VIVO_PUBLISHER_CLASS)); + } + + if (publisher != null) { + publisher.addProperty(model.createProperty(VIVO_PUBLISHER_OF), journal); + journal.addProperty(model.createProperty(VIVO_PUBLISHER), publisher); + } + } + } + + // Add relationships between our resource and the journal + if (journal != null) { + journal.addProperty(model.getProperty(VIVO_PUBLICATIONVENUEFOR), work); + work.addProperty(model.getProperty(VIVO_HASPUBLICATIONVENUE), journal); + } + } + } + + // Add an ISBN + if (resourceModel.ISBN != null && resourceModel.ISBN.length > 0) { + for (String isbn : resourceModel.ISBN) { + int length = getDigitCount(isbn); + if (length == 10) { + work.addProperty(model.getProperty(BIBO_ISBN10), isbn); + } else { + work.addProperty(model.getProperty(BIBO_ISBN13), isbn); + } + } + + if ("chapter".equals(resourceModel.type)) { + Resource book = null; + + String bookUri = findVIVOUriForISBNs(vreq.getRDFService(), resourceModel.ISBN); + if (StringUtils.isEmpty(bookUri)) { + book = model.createResource(getUnusedUri(vreq)); + + book.addProperty(RDFS.label, resourceModel.containerTitle); + book.addProperty(RDF.type, model.getResource(BIBO_BOOK)); + for (String isbn : resourceModel.ISBN) { + if (getDigitCount(isbn) == 10) { + book.addProperty(model.getProperty(BIBO_ISBN10), isbn); + } else { + book.addProperty(model.getProperty(BIBO_ISBN13), isbn); + } + } + + if (!StringUtils.isEmpty(resourceModel.publisher)) { + Resource publisher = model.createResource(getPublisherURI(vreq, resourceModel.publisher)); + publisher.addProperty(RDFS.label, resourceModel.publisher); + publisher.addProperty(RDF.type, model.getResource(VIVO_PUBLISHER_CLASS)); + publisher.addProperty(model.createProperty(VIVO_PUBLISHER_OF), book); + book.addProperty(model.createProperty(VIVO_PUBLISHER), publisher); + } + } else { + book = model.getResource(bookUri); + } + + if (book != null) { + book.addProperty(model.getProperty(VIVO_PUBLICATIONVENUEFOR), work); + work.addProperty(model.getProperty(VIVO_HASPUBLICATIONVENUE), book); + } + } + } + + // Add the volume + if (!StringUtils.isEmpty(resourceModel.volume)) { + work.addProperty(model.createProperty(BIBO_VOLUME), resourceModel.volume); + } + + // Add the issue + if (!StringUtils.isEmpty(resourceModel.issue)) { + work.addProperty(model.createProperty(BIBO_ISSUE), resourceModel.issue); + } + + // Add the page start + if (!StringUtils.isEmpty(resourceModel.pageStart)) { + work.addProperty(model.createProperty(BIBO_PAGE_START), resourceModel.pageStart); + } + + // Add the page end + if (!StringUtils.isEmpty(resourceModel.pageEnd)) { + work.addProperty(model.createProperty(BIBO_PAGE_END), resourceModel.pageEnd); + } + + // Add a page count + if (!StringUtils.isEmpty(resourceModel.pageStart) && !StringUtils.isEmpty(resourceModel.pageEnd)) { + try { + int pageStart = Integer.parseInt(resourceModel.pageStart, 10); + int pageEnd = Integer.parseInt(resourceModel.pageEnd, 10); + + if (pageStart > 0) { + if (pageEnd > pageStart) { + work.addLiteral(model.createProperty(BIBO_PAGE_COUNT), pageEnd - pageStart); + } else if (pageEnd == pageStart) { + work.addLiteral(model.createProperty(BIBO_PAGE_COUNT), 1); + } + } + } catch (NumberFormatException nfe) { + } + } + + // Add the publication date + addDateToResource(vreq, work, resourceModel.publicationDate); + + if (!StringUtils.isEmpty(resourceModel.abstractText)) { + work.addProperty(model.createProperty(BIBO_ABSTRACT), resourceModel.abstractText); + } + + // Add the authors + // Note - we start by creating VCARDs for all of the authors + // If the user has chosen an author position, this will be replaced later + if (resourceModel.author != null) { + int rank = 1; + for (ResourceModel.NameField author : resourceModel.author) { + if (author != null) { + Resource vcard = model.createResource(getVCardURI(vreq, author.family, author.given)); + vcard.addProperty(RDF.type, model.getResource(VCARD_INDIVIDUAL)); + + Resource name = model.createResource(getUnusedUri(vreq)); + vcard.addProperty(model.createProperty(VCARD_HAS_NAME), name); + name.addProperty(RDF.type, model.getResource(VCARD_NAME)); + if (!StringUtils.isEmpty(author.given)) { + name.addProperty(model.createProperty(VCARD_GIVENNAME), author.given); + } + if (!StringUtils.isEmpty(author.family)) { + name.addProperty(model.createProperty(VCARD_FAMILYNAME), author.family); + } + + Resource authorship = model.createResource(getUnusedUri(vreq)); + authorship.addProperty(RDF.type, model.getResource(VIVO_AUTHORSHIP)); + + authorship.addProperty(model.createProperty(VIVO_RELATES), model.getResource(vivoUri)); + authorship.addProperty(model.createProperty(VIVO_RELATES), model.getResource(vcard.getURI())); + + model.getResource(vivoUri).addProperty(model.createProperty(VIVO_RELATEDBY), authorship); + vcard.addProperty(model.createProperty(VIVO_RELATEDBY), authorship); + authorship.addLiteral(model.createProperty(VIVO_RANK), rank); + } + rank++; + } + } + + // Add a URL + if (!StringUtils.isEmpty(resourceModel.URL)) { + try { + URL url = new URL(resourceModel.URL); + Resource urlModel = model.createResource(getUnusedUri(vreq)); + urlModel.addProperty(RDF.type, model.getResource(VCARD_URL_CLASS)); + urlModel.addLiteral(model.createProperty(VCARD_URL_PROPERTY), url); + + Resource kindModel = model.createResource(getUnusedUri(vreq)); + kindModel.addProperty(RDF.type, model.getResource(VCARD_KIND)); + kindModel.addProperty(model.createProperty(VCARD_HAS_URL), urlModel); + kindModel.addProperty(model.createProperty(OBO_CONTACT_INFO_FOR), work); + + work.addProperty(model.createProperty(OBO_HAS_CONTACT_INFO), kindModel); + } catch (MalformedURLException e) { + } + } + + // editor + // translator + // subject + // status + // presented at + // keyword + + // http://purl.org/ontology/bibo/status + // http://vivoweb.org/ontology/core#hasSubjectArea + // http://purl.org/ontology/bibo/presentedAt + // http://vivoweb.org/ontology/core#freetextKeyword + // http://purl.org/ontology/bibo/translator (Direct to user) + + // http://vivoweb.org/ontology/core#Editorship + + return vivoUri; + } + + /** + * Get a URI for the publisher object + * + * @param vreq + * @param publisher + * @return + */ + protected String getPublisherURI(VitroRequest vreq, String publisher) { + return getUnusedUri(vreq); + } + + /** + * Get a URI for a VCARD object + * + * @param vreq + * @param familyName + * @param givenName + * @return + */ + protected String getVCardURI(VitroRequest vreq, String familyName, String givenName) { + return getUnusedUri(vreq); + } + + /** + * Add a date object to the resource + * + * @param vreq + * @param work + * @param date + * @return + */ + protected boolean addDateToResource(VitroRequest vreq, Resource work, ResourceModel.DateField date) { + Model model = work.getModel(); + + if (date == null || date.year == null) { + return false; + } + + String formattedDate = null; + String precision = null; + + if (date.month != null) { + if (date.day != null) { + formattedDate = String.format("%04d-%02d-%02dT00:00:00", date.year, date.month, date.day); + precision = "http://vivoweb.org/ontology/core#dayPrecision"; + } else { + formattedDate = String.format("%04d-%02d-01T00:00:00", date.year, date.month); + precision = "http://vivoweb.org/ontology/core#monthPrecision"; + } + } else { + formattedDate = String.format("%04d-01-01T00:00:00", date.year); + precision = "http://vivoweb.org/ontology/core#yearPrecision"; + } + + Resource dateResource = model.createResource(getUnusedUri(vreq)).addProperty(RDF.type, model.getResource("http://vivoweb.org/ontology/core#DateTimeValue")); + dateResource.addProperty(model.createProperty(VIVO_DATETIME), formattedDate); + dateResource.addProperty(model.createProperty(VIVO_DATETIMEPRECISION), precision); + + work.addProperty(model.createProperty(VIVO_DATETIMEVALUE), dateResource); + return true; + } + + /** + * Get an unused standard Uri from VIVO + * + * @param vreq + * @return + */ + private String getUnusedUri(VitroRequest vreq) { + NewURIMakerVitro uriMaker = new NewURIMakerVitro(vreq.getWebappDaoFactory()); + try { + return uriMaker.getUnusedNewURI(null); + } catch (InsertException e) { + } + + return null; + } + + /** + * Find a Uri for an ISSN + * + * @param rdfService + * @param issns + * @return + */ + private String findVIVOUriForISSNs(RDFService rdfService, String[] issns) { + // First look to see if any journals already define the ISSN + if (issns != null && issns.length > 0) { + for (String issn : issns) { + final List journals = new ArrayList<>(); + String query = "SELECT ?journal\n" + + "WHERE\n" + + "{\n" + + " {\n" + + " \t?journal \"" + issn + "\" .\n" + + " }\n" + + "}\n"; + + try { + rdfService.sparqlSelectQuery(query, new ResultSetConsumer() { + @Override + protected void processQuerySolution(QuerySolution qs) { + Resource journal = qs.getResource("journal"); + if (journal != null) { + journals.add(journal.getURI()); + } + } + }); + } catch (RDFServiceException e) { + } + + // We've found a journal that matches, so use that + if (journals.size() > 0) { + return journals.get(0); + } + } + } + + // No journal found, so return null + return null; + } + + /** + * Find a Uri for an ISBN + * + * @param rdfService + * @param isbns + * @return + */ + private String findVIVOUriForISBNs(RDFService rdfService, String[] isbns) { + // First look to see if any journals already define the ISSN + if (isbns != null && isbns.length > 0) { + for (String isbn : isbns) { + final List books = new ArrayList<>(); + String query = "SELECT ?book\n" + + "WHERE\n" + + "{\n" + + " {\n" + + (getDigitCount(isbn) == 10 ? + " \t?book \"" + isbn + "\" .\n" : + " \t?book \"" + isbn + "\" .\n" ) + + " \t?book a .\n" + + " }\n" + + "}\n"; + + try { + rdfService.sparqlSelectQuery(query, new ResultSetConsumer() { + @Override + protected void processQuerySolution(QuerySolution qs) { + Resource book = qs.getResource("book"); + if (book != null) { + books.add(book.getURI()); + } + } + }); + } catch (RDFServiceException e) { + } + + // We've found a book that matches, so use that + if (books.size() > 0) { + for (String url : books) { + if (url.contains("doi")) { + return url; + } + } + + return books.get(0); + } + } + } + + // No books found, so return null + return null; + } + + /** + * Find a Uri for an Publisher + * + * @param rdfService + * @param publisher The name of the publisher + * @return + */ + private String findVIVOUriForPublisher(RDFService rdfService, String publisher) { + // First look to see if any publishers already have that label + final List publisherUris = new ArrayList<>(); + String query = "SELECT ?publisher\n" + + "WHERE\n" + + "{\n" + + " {\n" + + " \t?publisher a <" + VIVO_PUBLISHER_CLASS + "> .\n" + + " \t?publisher <" + RDFS.label + "> \"" + publisher + "\" .\n" + + " }\n" + + "}\n"; + + try { + rdfService.sparqlSelectQuery(query, new ResultSetConsumer() { + @Override + protected void processQuerySolution(QuerySolution qs) { + Resource publisherUri = qs.getResource("publisher"); + if (publisherUri != null) { + publisherUris.add(publisherUri.getURI()); + } + } + }); + } catch (RDFServiceException e) { + } + + // We've found a publisher that matches, so use that + if (publisherUris.size() > 0) { + return publisherUris.get(0); + } + + return null; + } + + /** + * Find a Uri for a DOI + * + * @param rdfService + * @param doi + * @return + */ + private String findVIVOUriForDOI(RDFService rdfService, String doi) { + // First, find a resource that already defines the DOI + if (!StringUtils.isEmpty(doi)) { + final List works = new ArrayList<>(); + String query = "SELECT ?work\n" + + "WHERE\n" + + "{\n" + + " {\n" + + " \t?work \"" + doi + "\" .\n" + + " }\n" + + " UNION\n" + + " {\n" + + " \t?work \"http://doi.org/" + doi + "\" .\n" + + " }\n" + + " UNION\n" + + " {\n" + + " \t?work \"https://doi.org/" + doi + "\" .\n" + + " }\n" + + " UNION\n" + + " {\n" + + " \t?work \"http://dx.doi.org/" + doi + "\" .\n" + + " }\n" + + " UNION\n" + + " {\n" + + " \t?work \"https://dx.doi.org/" + doi + "\" .\n" + + " }\n" + + "}\n"; + + try { + rdfService.sparqlSelectQuery(query, new ResultSetConsumer() { + @Override + protected void processQuerySolution(QuerySolution qs) { + Resource work = qs.getResource("work"); + if (work != null) { + works.add(work.getURI()); + } + } + }); + } catch (RDFServiceException e) { + } + + // We've found a resource, so return it's Uri + if (works.size() > 0) { + return works.get(0); + } + } + + // No resource found with the DOI, so return null + return null; + } + + /** + * Count the number of digits in a string + * + * @param id + * @return + */ + private int getDigitCount(String id) { + int digits = 0; + + if (id != null) { + for (char ch : id.toCharArray()) { + if (Character.isDigit(ch)) { + digits++; + } + } + } + + return digits; + } + + /** + * Find a Uri for a resource that defines a PubMed ID + * @param rdfService + * @param pmid + * @return + */ + private String findVIVOUriForPubMedID(RDFService rdfService, String pmid) { + // Look for a resource that defines the PubMed ID + if (!StringUtils.isEmpty(pmid)) { + final List works = new ArrayList<>(); + String query = "SELECT ?work\n" + + "WHERE\n" + + "{\n" + + " {\n" + + " \t?work \"" + pmid + "\" .\n" + + " }\n" + + "}\n"; + + try { + rdfService.sparqlSelectQuery(query, new ResultSetConsumer() { + @Override + protected void processQuerySolution(QuerySolution qs) { + Resource work = qs.getResource("work"); + if (work != null) { + works.add(work.getURI()); + } + } + }); + } catch (RDFServiceException e) { + } + + // If we have a resource, return the Uri + if (works.size() == 1) { + return works.get(0); + } + } + + // No resource found, so return null + return null; + } + + /** + * Try to find a resource in VIVO that defines one of the external identifiers + * @param vreq + * @param ids + * @param profileUri + * @param citation + * @return + */ + protected String findInVIVO(VitroRequest vreq, ExternalIdentifiers ids, String profileUri, Citation citation) { + // First, look for a resource that defines the DOI + String vivoUri = findVIVOUriForDOI(vreq.getRDFService(), ids.DOI); + + // No DOI, so look for a resource that defines the PubMed ID + if (StringUtils.isEmpty(vivoUri)) { + vivoUri = findVIVOUriForPubMedID(vreq.getRDFService(), ids.PubMedID); + } + + // If we have been passed a citation object, and have found a resource, populate the citation object + if (citation != null && !StringUtils.isEmpty(vivoUri)) { + // Get a moel for the resource + Model model = getExistingResource(vreq, vivoUri); + + // Get the resource from the model + Resource work = model.getResource(vivoUri); + String pageStart = null; + String pageEnd = null; + Citation.Name[] rankedAuthors = null; + ArrayList unrankedAuthors = new ArrayList<>(); + + // Loop through all the statements on the resource + StmtIterator stmtIterator = work.listProperties(); + try { + while (stmtIterator.hasNext()) { + Statement stmt = stmtIterator.next(); + + switch (stmt.getPredicate().getURI()) { + case RDFS_LABEL: + citation.title = stmt.getString(); + break; + + case BIBO_VOLUME: + citation.volume = stmt.getString(); + break; + + case BIBO_ISSUE: + citation.issue = stmt.getString(); + break; + + case BIBO_PAGE_START: + pageStart = stmt.getString(); + break; + + case BIBO_PAGE_END: + pageEnd = stmt.getString(); + break; + + // Publication date + case VIVO_DATETIMEVALUE: + Resource dateTime = stmt.getResource(); + if (dateTime != null) { + Statement stmtDate = dateTime.getProperty(model.getProperty(VIVO_DATETIME)); + if (stmtDate != null) { + String dateTimeValue = stmtDate.getString(); + if (dateTimeValue != null && dateTimeValue.length() > 3) { + citation.publicationYear = Integer.parseInt(dateTimeValue.substring(0, 4), 10); + } + } + } + break; + + // Journal + case VIVO_HASPUBLICATIONVENUE: + Resource journal = stmt.getResource(); + if (journal != null) { + Statement stmtJournalName = journal.getProperty(RDFS.label); + if (stmtJournalName != null) { + citation.journal = stmtJournalName.getString(); + } + } + break; + + // Relationships - we are really interested in authors and editors + case VIVO_RELATEDBY: + // Get the relationship context + Resource relationship = stmt.getResource(); + if (relationship != null) { + Integer rank = null; + + // If it isn't an authorship or editorship, skip it + if (!isResourceOfType(relationship, VIVO_AUTHORSHIP) && + !isResourceOfType(relationship, VIVO_EDITORSHIP)) { + break; + } + + // Now loop over the properties of the author/editorship context + Resource personResource = null; + StmtIterator relationshipIter = relationship.listProperties(); + try { + while (relationshipIter.hasNext()) { + Statement relationshipStmt = relationshipIter.next(); + switch (relationshipStmt.getPredicate().getURI()) { + // If it is a relates property + case VIVO_RELATES: + // If it isn't pointing to the resource, it must be pointing to a person + if (!vivoUri.equals(relationshipStmt.getResource().getURI())) { + personResource = relationshipStmt.getResource(); + } + break; + + // Author position + case VIVO_RANK: + rank = relationshipStmt.getInt(); + break; + } + } + } finally { + relationshipIter.close(); + } + + Citation.Name newAuthor = null; + + // If we've got an author + if (personResource != null) { + // If the author is the user, then they have already claimed this publication + if (profileUri.equals(personResource.getURI())) { + citation.alreadyClaimed = true; + } + + if (isResourceOfType(relationship, VIVO_AUTHORSHIP)) { + boolean linked = false; + + // Now get the name of the author, from either the VCARD or the foaf:Person + Statement familyName = null; + Statement givenName = null; + if (isResourceOfType(personResource, VCARD_INDIVIDUAL)) { + if (personResource.hasProperty(model.getProperty(VCARD_HAS_NAME))) { + Resource vcardName = personResource.getPropertyResourceValue(model.getProperty(VCARD_HAS_NAME)); + if (vcardName != null) { + if (vcardName.hasProperty(model.getProperty(VCARD_GIVENNAME))) { + givenName = vcardName.getProperty(model.getProperty(VCARD_GIVENNAME)); + } + if (vcardName.hasProperty(model.getProperty(VCARD_FAMILYNAME))) { + familyName = vcardName.getProperty(model.getProperty(VCARD_FAMILYNAME)); + } + } + } + } else if (personResource.hasProperty(model.getProperty(OBO_HAS_CONTACT_INFO))) { + Resource vCard = personResource.getPropertyResourceValue(model.getProperty(OBO_HAS_CONTACT_INFO)); + if (vCard.hasProperty(model.getProperty(VCARD_HAS_NAME))) { + Resource vcardName = vCard.getPropertyResourceValue(model.getProperty(VCARD_HAS_NAME)); + if (vcardName != null) { + if (vcardName.hasProperty(model.getProperty(VCARD_GIVENNAME))) { + givenName = vcardName.getProperty(model.getProperty(VCARD_GIVENNAME)); + } + if (vcardName.hasProperty(model.getProperty(VCARD_FAMILYNAME))) { + familyName = vcardName.getProperty(model.getProperty(VCARD_FAMILYNAME)); + } + } + linked = true; + } + } + + if (givenName == null) { + // It's a foaf person, which means it is already linked to a full profile in VIVO + if (personResource.hasProperty(model.getProperty(FOAF_FIRSTNAME))) { + givenName = personResource.getProperty(model.getProperty(FOAF_FIRSTNAME)); + } + if (personResource.hasProperty(model.getProperty(FOAF_LASTNAME))) { + familyName = personResource.getProperty(model.getProperty(FOAF_LASTNAME)); + } + linked = true; + } + + // If we have an author name, format it + if (familyName != null) { + newAuthor = new Citation.Name(); + if (givenName != null) { + newAuthor.name = CreateAndLinkUtils.formatAuthorString(familyName.getString(), givenName.getString()); + } else { + newAuthor.name = CreateAndLinkUtils.formatAuthorString(familyName.getString(), null); + } + + // Record whether the author is a full profile, or just a VCARD + newAuthor.linked = linked; + } else { + Statement label = personResource.getProperty(RDFS.label); + if (label != null) { + String name = label.getString(); + if (name.contains(",")) { + String[] parts = name.split("\\s*,\\s*"); + if (parts.length > 1) { + name = CreateAndLinkUtils.formatAuthorString(parts[0], parts[parts.length - 1]); + } + } else { + String[] parts = name.split("\\s*"); + if (parts.length > 1) { + name = CreateAndLinkUtils.formatAuthorString(parts[parts.length - 1], parts[0]); + } + } + + newAuthor = new Citation.Name(); + newAuthor.name = name; + newAuthor.linked = linked; + } + } + } + } else { + if (isResourceOfType(relationship, VIVO_AUTHORSHIP)) { + newAuthor = new Citation.Name(); + newAuthor.name = "Deleted Author"; + } + } + + // If we have an author + if (newAuthor != null) { + // If we have an author position, insert it in the correct place of the ranked authors + if (rank != null) { + if (rankedAuthors == null) { + rankedAuthors = new Citation.Name[rank]; + } else if (rankedAuthors.length < rank) { + Citation.Name[] newAuthors = new Citation.Name[rank]; + for (int i = 0; i < rankedAuthors.length; i++) { + newAuthors[i] = rankedAuthors[i]; + } + rankedAuthors = newAuthors; + } + rankedAuthors[rank - 1] = newAuthor; + } else { + // Unranked author, so just keep hold of it to add at the end + unrankedAuthors.add(newAuthor); + } + } + } + break; + } + } + } finally { + stmtIterator.close(); + } + + // Create the pagination field + if (!StringUtils.isEmpty(pageStart)) { + if (!StringUtils.isEmpty(pageEnd)) { + citation.pagination = pageStart + "-" + pageEnd; + } else { + citation.pagination = pageStart; + } + } + + // If we have unranked authors, add them to the end of the ranked authors + if (unrankedAuthors.size() > 0) { + if (rankedAuthors == null) { + citation.authors = unrankedAuthors.toArray(new Citation.Name[unrankedAuthors.size()]); + } else { + Citation.Name[] newAuthors = new Citation.Name[rankedAuthors.length + unrankedAuthors.size()]; + int i = 0; + while (i < rankedAuthors.length) { + newAuthors[i] = rankedAuthors[i]; + i++; + } + while (i < newAuthors.length && unrankedAuthors.size() > 0) { + newAuthors[i] = unrankedAuthors.remove(0); + i++; + } + citation.authors = newAuthors; + } + } else { + citation.authors = rankedAuthors; + } + } + + // Return the uri of the resource (or null) + return vivoUri; + } + + /** + * Check that the resource is declared to be of a particular type + * + * @param resource + * @param typeUri + * @return + */ + protected boolean isResourceOfType(Resource resource, String typeUri) { + if (resource == null) { + return false; + } + + StmtIterator iter = resource.listProperties(RDF.type); + try { + while (iter.hasNext()) { + Statement stmt = iter.next(); + if (typeUri.equals(stmt.getResource().getURI())) { + return true; + } + } + } finally { + iter.close(); + } + + return false; + } + + /** + * Determine the difference between the "existing" and "updated" models, and write the changes to VIVO + * + * @param rdfService + * @param existingModel + * @param updatedModel + */ + protected void writeChanges(RDFService rdfService, Model existingModel, Model updatedModel) { + Model removeModel = existingModel.difference(updatedModel); + Model addModel = updatedModel.difference(existingModel); + + if (!addModel.isEmpty() || !removeModel.isEmpty()) { + InputStream addStream = null; + InputStream removeStream = null; + + InputStream is = makeN3InputStream(updatedModel); + ChangeSet changeSet = rdfService.manufactureChangeSet(); + + if (!addModel.isEmpty()) { + addStream = makeN3InputStream(addModel); + changeSet.addAddition(addStream, RDFService.ModelSerializationFormat.N3, ModelNames.ABOX_ASSERTIONS); + } + + if (!removeModel.isEmpty()) { + removeStream = makeN3InputStream(removeModel); + changeSet.addRemoval(removeStream, RDFService.ModelSerializationFormat.N3, ModelNames.ABOX_ASSERTIONS); + } + + try { + rdfService.changeSetUpdate(changeSet); + } catch (RDFServiceException e) { + } finally { + if (addStream != null) { + try { addStream.close(); } catch (IOException e) { } + } + + if (removeStream != null) { + try { removeStream.close(); } catch (IOException e) { } + } + } + } + } + + /** + * Convert the model into an N3 stream + * + * @param m + * @return + */ + private InputStream makeN3InputStream(Model m) { + ByteArrayOutputStream out = new ByteArrayOutputStream(); + m.write(out, "N3"); + return new ByteArrayInputStream(out.toByteArray()); + } + + /** + * Get a list of publication types - classes that are a bibo:Document - from the triple store + * @param vreq + * @return + */ + private List getPublicationTypes(VitroRequest vreq) { + // List of publication types to return (final so it can be used in the ResultSetConsumer + final List types = new ArrayList<>(); + + try { + // Find all classes that are a subclass of Document, and their labels + String query = "PREFIX rdfs: \n" + + "\n" + + "SELECT ?uri ?label\n" + + "WHERE {\n" + + "\t?uri rdfs:label ?label .\n" + + "\t?uri rdfs:subClassOf .\n" + + "}\n"; + + vreq.getRDFService().sparqlSelectQuery(query, new ResultSetConsumer() { + @Override + protected void processQuerySolution(QuerySolution qs) { + // Add a publication type to the list + types.add(new PublicationType( + qs.getLiteral("label").getString(), + qs.getResource("uri").getURI() + )); + } + }); + } catch (RDFServiceException e) { + } + + // Sort the publication type list + types.sort(new Comparator() { + @Override + public int compare(PublicationType o1, PublicationType o2) { + return o1.getLabel().compareTo(o2.getLabel()); + } + }); + + return types; + } + + public class PublicationType { + private String label; + private String uri; + + public PublicationType(String label, String uri) { + this.label = label; + this.uri = uri; + } + + public String getLabel() { + return label; + } + + public String getUri() { + return uri; + } + } +} + + diff --git a/api/src/main/java/org/vivoweb/webapp/createandlink/Citation.java b/api/src/main/java/org/vivoweb/webapp/createandlink/Citation.java new file mode 100644 index 00000000..4c682590 --- /dev/null +++ b/api/src/main/java/org/vivoweb/webapp/createandlink/Citation.java @@ -0,0 +1,79 @@ +/* $This file is distributed under the terms of the license in /doc/license.txt$ */ + +package org.vivoweb.webapp.createandlink; + +public class Citation { + public String externalId; + public String externalProvider; + public String externalResource; + + public String vivoUri; + + public String type; + public String typeUri; + public String title; + public Name[] authors; + public String journal; + public String volume; + public String issue; + public String pagination; + public Integer publicationYear; + public String DOI; + + public boolean alreadyClaimed = false; + public boolean showError = false; + + public String getExternalId() { return externalId; } + public String getExternalProvider() { return externalProvider; } + public String getExternalResource() { return externalResource; } + + public String getVivoUri() { return vivoUri; } + + public String getType() { return type; } + public String getTypeUri() { return typeUri; } + public String getTitle() { return title; } + public Name[] getAuthors() { + return authors; + } + public String getJournal() { + return journal; + } + public String getVolume() { + return volume; + } + public String getIssue() { + return issue; + } + public String getPagination() { + return pagination; + } + public Integer getPublicationYear() { + return publicationYear; + } + + public String getDOI() { + return DOI; + } + + public boolean getAlreadyClaimed() { return alreadyClaimed; } + + public boolean getShowError() { return showError; } + + public static class Name { + public String name; + + public boolean linked = false; + public boolean proposed = false; + + public String getName() { + return name; + } + + public boolean getLinked() { + return linked; + } + public boolean getProposed() { + return proposed; + } + } +} diff --git a/api/src/main/java/org/vivoweb/webapp/createandlink/CiteprocJSONModel.java b/api/src/main/java/org/vivoweb/webapp/createandlink/CiteprocJSONModel.java new file mode 100644 index 00000000..410996a3 --- /dev/null +++ b/api/src/main/java/org/vivoweb/webapp/createandlink/CiteprocJSONModel.java @@ -0,0 +1,139 @@ +/* $This file is distributed under the terms of the license in /doc/license.txt$ */ + +package org.vivoweb.webapp.createandlink; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonProperty; + +@JsonIgnoreProperties +public class CiteprocJSONModel { + public String type; + public String id; // Number? + public String[] categories; + public String language; + public String journalAbbreviation; + public String shortTitle; + public NameField[] author; + @JsonProperty("collection-editor") + public NameField[] collectionEditor; + public NameField[] composer; + @JsonProperty("container-author") + public NameField[] containerAuthor; + public NameField[] director; + public NameField[] editor; + @JsonProperty("editorial-director") + public NameField[] editorialDirector; + public NameField[] interviewer; + public NameField[] illustrator; + @JsonProperty("original-author") + public NameField[] originalAuthor; + public NameField[] recipient; + @JsonProperty("reviewed-author") + public NameField[] reviewedAuthor; + public NameField[] translator; + public DateField accessed; + public DateField container; + @JsonProperty("event-date") + public DateField eventDate; + public DateField issued; + @JsonProperty("original-date") + public DateField originalDate; + public DateField submitted; + @JsonProperty("abstract") + public String abstractText; + public String annote; + public String archive; + public String archive_location; + public String authority; + @JsonProperty("call-number") + public String callNumber; + @JsonProperty("chapter-number") + public String chapterNumber; + @JsonProperty("citation-number") + public String citationNumber; + @JsonProperty("citation-label") + public String citationLabel; + @JsonProperty("collection-number") + public String collectionNumber; + @JsonProperty("container-title") + public String containerTitle; + @JsonProperty("container-title-short") + public String containerTitleShort; + public String dimensions; + public String DOI; + public String edition; // Integer? + public String event; + @JsonProperty("event-place") + public String eventPlace; + @JsonProperty("first-reference-note-number") + public String firstReferenceNoteNumber; + public String genre; + public String ISBN; + public String ISSN; + public String issue; // Integer? + public String jurisdiction; + public String keyword; + public String locator; + public String medium; + public String note; + public String number; // Integer? + @JsonProperty("number-of-pages") + public String numberOfPages; + @JsonProperty("number-of-volumes") + public String numberOfVolumes; // Integer? + @JsonProperty("original-publisher") + public String originalPublisher; + @JsonProperty("original-publisher-place") + public String originalPublisherPlace; + @JsonProperty("original-title") + public String originalTitle; + public String page; + @JsonProperty("page-first") + public String pageFirst; + public String PMCID; + public String PMID; + public String publisher; + @JsonProperty("publisher-place") + public String publisherPlace; + public String references; + @JsonProperty("reviewed-title") + public String reviewedTitle; + public String scale; + public String section; + public String source; + public String status; + public String title; + @JsonProperty("title-short") + public String titleShort; + public String URL; + public String version; + public String volume; // Integer? + @JsonProperty("year-suffix") + public String yearSuffix; + + public static class NameField { + public String family; + public String given; + @JsonProperty("dropping-particle") + public String droppingParticle; + @JsonProperty("non-dropping-particle") + public String nonDroppingParticle; + public String suffix; + @JsonProperty("comma-suffix") + public String commaSuffix; // Number? Boolean? + @JsonProperty("staticOrdering") + public String staticOrdering; // Number? Boolean? + public String literal; + @JsonProperty("parse-names") + public String parseNames; // Number? Boolean? + } + + public static class DateField { + @JsonProperty("date-parts") + public String[][] dateParts; // Number? + public String season; // Number? + public String circa; // Number? Boolean? + public String literal; + public String raw; + } +} diff --git a/api/src/main/java/org/vivoweb/webapp/createandlink/ContributorRole.java b/api/src/main/java/org/vivoweb/webapp/createandlink/ContributorRole.java new file mode 100644 index 00000000..320a79b7 --- /dev/null +++ b/api/src/main/java/org/vivoweb/webapp/createandlink/ContributorRole.java @@ -0,0 +1,19 @@ +/* $This file is distributed under the terms of the license in /doc/license.txt$ */ + +package org.vivoweb.webapp.createandlink; + +public class ContributorRole { + private String key; + private String label; + private String uri; + + public ContributorRole(String key, String label, String uri) { + this.key = key; + this.label = label; + this.uri = uri; + } + + public String getKey() { return key; } + public String getLabel() { return label; } + public String getUri() { return uri; } +} diff --git a/api/src/main/java/org/vivoweb/webapp/createandlink/CreateAndLinkResourceProvider.java b/api/src/main/java/org/vivoweb/webapp/createandlink/CreateAndLinkResourceProvider.java new file mode 100644 index 00000000..044bc992 --- /dev/null +++ b/api/src/main/java/org/vivoweb/webapp/createandlink/CreateAndLinkResourceProvider.java @@ -0,0 +1,15 @@ +/* $This file is distributed under the terms of the license in /doc/license.txt$ */ + +package org.vivoweb.webapp.createandlink; + +public interface CreateAndLinkResourceProvider { + String normalize(String id); + + String getLabel(); + + ExternalIdentifiers allExternalIDsForFind(String externalId); + + String findInExternal(String id, Citation citation); + + ResourceModel makeResourceModel(String externalId, String externalResource); +} diff --git a/api/src/main/java/org/vivoweb/webapp/createandlink/CreateAndLinkUtils.java b/api/src/main/java/org/vivoweb/webapp/createandlink/CreateAndLinkUtils.java new file mode 100644 index 00000000..52864585 --- /dev/null +++ b/api/src/main/java/org/vivoweb/webapp/createandlink/CreateAndLinkUtils.java @@ -0,0 +1,34 @@ +/* $This file is distributed under the terms of the license in /doc/license.txt$ */ + +package org.vivoweb.webapp.createandlink; + +import org.apache.commons.lang3.StringUtils; + +public class CreateAndLinkUtils { + public static String formatAuthorString(String familyName, String givenName) { + if (StringUtils.isEmpty(familyName)) { + return null; + } + + StringBuilder authorBuilder = new StringBuilder(familyName); + + if (!StringUtils.isEmpty(givenName)) { + authorBuilder.append(", "); + boolean addToAuthor = true; + for (char ch : givenName.toCharArray()) { + if (addToAuthor) { + if (Character.isAlphabetic(ch)) { + authorBuilder.append(Character.toUpperCase(ch)); + addToAuthor = false; + } + } else { + if (!Character.isAlphabetic(ch)) { + addToAuthor = true; + } + } + } + } + + return authorBuilder.toString(); + } +} diff --git a/api/src/main/java/org/vivoweb/webapp/createandlink/ExternalIdentifiers.java b/api/src/main/java/org/vivoweb/webapp/createandlink/ExternalIdentifiers.java new file mode 100644 index 00000000..8d5595b2 --- /dev/null +++ b/api/src/main/java/org/vivoweb/webapp/createandlink/ExternalIdentifiers.java @@ -0,0 +1,9 @@ +/* $This file is distributed under the terms of the license in /doc/license.txt$ */ + +package org.vivoweb.webapp.createandlink; + +public class ExternalIdentifiers { + public String DOI; + public String PubMedID; // http://www.ncbi.nlm.nih.gov/pmc/utils/idconv/v1.0/?ids=23193287&format=json + public String PubMedCentralID; // http://www.ncbi.nlm.nih.gov/pmc/utils/idconv/v1.0/?ids=PMC3531190&format=json +} diff --git a/api/src/main/java/org/vivoweb/webapp/createandlink/ResourceModel.java b/api/src/main/java/org/vivoweb/webapp/createandlink/ResourceModel.java new file mode 100644 index 00000000..e72c0cd3 --- /dev/null +++ b/api/src/main/java/org/vivoweb/webapp/createandlink/ResourceModel.java @@ -0,0 +1,46 @@ +/* $This file is distributed under the terms of the license in /doc/license.txt$ */ + +package org.vivoweb.webapp.createandlink; + +public class ResourceModel { + public String DOI; + public String PubMedID; + public String PubMedCentralID; + public String[] ISSN; + public String[] ISBN; + public String URL; + + public NameField[] author; + public NameField[] editor; + public NameField[] translator; + + public String containerTitle; + public String issue; + public String pageStart; + public String pageEnd; + + public DateField publicationDate; + + public String publisher; + + public String[] subject; + public String title; + public String type; + public String volume; + + public String status; + public String presentedAt; + public String[] keyword; + public String abstractText; + + public static class NameField { + public String family; + public String given; + } + + public static class DateField { + public Integer year; + public Integer month; + public Integer day; + } +} diff --git a/api/src/main/java/org/vivoweb/webapp/createandlink/crossref/CrossrefCiteprocJSONModel.java b/api/src/main/java/org/vivoweb/webapp/createandlink/crossref/CrossrefCiteprocJSONModel.java new file mode 100644 index 00000000..76338c83 --- /dev/null +++ b/api/src/main/java/org/vivoweb/webapp/createandlink/crossref/CrossrefCiteprocJSONModel.java @@ -0,0 +1,191 @@ +/* $This file is distributed under the terms of the license in /doc/license.txt$ */ + +package org.vivoweb.webapp.createandlink.crossref; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.databind.annotation.JsonDeserialize; +import org.vivoweb.webapp.createandlink.utils.StringArrayDeserializer; + +import java.util.Date; + +/** + * Note that ISSN and ISBN are arrays in Crossref, whereas Citeproc defines them to be a single value. + * + */ +@JsonIgnoreProperties +public class CrossrefCiteprocJSONModel { + // Crossref Specific Fields + + @JsonDeserialize(using = StringArrayDeserializer.class) + public String[] ISSN; + @JsonDeserialize(using = StringArrayDeserializer.class) + public String[] ISBN; + + public DateField created; +// public DateField deposited; +// public DateField indexed; + +// public String member; + public String prefix; + + @JsonProperty("article-number") + public String articleNumber; + + @JsonProperty("published-online") + public DateField publishedOnline; + + @JsonProperty("published-print") + public DateField publishedPrint; + +// @JsonProperty("reference-count") +// public Integer referenceCount; + public Double score; + @JsonDeserialize(using = StringArrayDeserializer.class) + public String[] subject; +// public String[] subtitle; + + // Standard Citeproc fields + + public String type; + public String id; // Number? +// public String[] categories; + public String language; +// public String journalAbbreviation; +// public String shortTitle; + public NameField[] author; +// @JsonProperty("collection-editor") +// public NameField[] collectionEditor; +// public NameField[] composer; +// @JsonProperty("container-author") +// public NameField[] containerAuthor; +// public NameField[] director; + public NameField[] editor; + @JsonProperty("editorial-director") +// public NameField[] editorialDirector; +// public NameField[] interviewer; +// public NameField[] illustrator; +// @JsonProperty("original-author") +// public NameField[] originalAuthor; +// public NameField[] recipient; +// @JsonProperty("reviewed-author") +// public NameField[] reviewedAuthor; + public NameField[] translator; +// public DateField accessed; + public DateField container; +// @JsonProperty("event-date") +// public DateField eventDate; + public DateField issued; +// @JsonProperty("original-date") +// public DateField originalDate; + public DateField submitted; + @JsonProperty("abstract") + public String abstractText; +// public String annote; +// public String archive; +// public String archive_location; +// public String authority; +// @JsonProperty("call-number") +// public String callNumber; +// @JsonProperty("chapter-number") +// public String chapterNumber; +// @JsonProperty("citation-number") +// public String citationNumber; +// @JsonProperty("citation-label") +// public String citationLabel; +// @JsonProperty("collection-number") +// public String collectionNumber; + @JsonProperty("container-title") + public String containerTitle; +// @JsonProperty("container-title-short") +// public String containerTitleShort; +// public String dimensions; + public String DOI; +// public String edition; // Integer? + public String event; +// @JsonProperty("event-place") +// public String eventPlace; +// @JsonProperty("first-reference-note-number") +// public String firstReferenceNoteNumber; +// public String genre; + public String issue; // Integer? +// public String jurisdiction; +// public String keyword; +// public String locator; +// public String medium; + public String note; + public String number; // Integer? +// @JsonProperty("number-of-pages") +// public String numberOfPages; +// @JsonProperty("number-of-volumes") +// public String numberOfVolumes; // Integer? +// @JsonProperty("original-publisher") +// public String originalPublisher; +// @JsonProperty("original-publisher-place") +// public String originalPublisherPlace; +// @JsonProperty("original-title") +// public String originalTitle; + public String page; +// @JsonProperty("page-first") +// public String pageFirst; + public String PMCID; + public String PMID; + public String publisher; +// @JsonProperty("publisher-place") +// public String publisherPlace; +// public String references; +// @JsonProperty("reviewed-title") +// public String reviewedTitle; + public String scale; + public String section; + public String source; + public String status; + public String title; +// @JsonProperty("title-short") +// public String titleShort; + public String URL; + public String version; + public String volume; // Integer? +// @JsonProperty("year-suffix") +// public String yearSuffix; + + public static class NameField { + // Crossref specific fields + +// public String[] affiliation; + + // Standard Citeproc fields + + public String family; + public String given; +// @JsonProperty("dropping-particle") +// public String droppingParticle; +// @JsonProperty("non-dropping-particle") +// public String nonDroppingParticle; + public String suffix; +// @JsonProperty("comma-suffix") +// public String commaSuffix; // Number? Boolean? +// @JsonProperty("staticOrdering") +// public String staticOrdering; // Number? Boolean? + public String literal; +// @JsonProperty("parse-names") +// public String parseNames; // Number? Boolean? + } + + public static class DateField { + // Crossref specific fields + + @JsonProperty("date-time") + public Date dateTime; +// public Long timestamp; + + // Standard Citeproc fields + + @JsonProperty("date-parts") + public String[][] dateParts; // Number? +// public String season; // Number? +// public String circa; // Number? Boolean? + public String literal; +// public String raw; + } +} diff --git a/api/src/main/java/org/vivoweb/webapp/createandlink/crossref/CrossrefCreateAndLinkResourceProvider.java b/api/src/main/java/org/vivoweb/webapp/createandlink/crossref/CrossrefCreateAndLinkResourceProvider.java new file mode 100644 index 00000000..d7402a1c --- /dev/null +++ b/api/src/main/java/org/vivoweb/webapp/createandlink/crossref/CrossrefCreateAndLinkResourceProvider.java @@ -0,0 +1,115 @@ +/* $This file is distributed under the terms of the license in /doc/license.txt$ */ + +package org.vivoweb.webapp.createandlink.crossref; + +import org.vivoweb.webapp.createandlink.Citation; +import org.vivoweb.webapp.createandlink.CreateAndLinkResourceProvider; +import org.vivoweb.webapp.createandlink.ExternalIdentifiers; +import org.vivoweb.webapp.createandlink.ResourceModel; + +/** + * Provider for looking up DOIs in CrossRef + */ +public class CrossrefCreateAndLinkResourceProvider implements CreateAndLinkResourceProvider { + /** + * Make a normalized version of the ID + * + * @param id + * @return + */ + @Override + public String normalize(String id) { + if (id != null) { + // Trim and lower case + String doiTrimmed = id.trim().toLowerCase(); + + // If we have been passed the resolver URI, strip it down to the bare DOI + if (doiTrimmed.startsWith("https://dx.doi.org/")) { + return doiTrimmed.substring(19); + } else if (doiTrimmed.startsWith("http://dx.doi.org/")) { + return doiTrimmed.substring(18); + } else if (doiTrimmed.startsWith("https://doi.org/")) { + return doiTrimmed.substring(16); + } else if (doiTrimmed.startsWith("http://doi.org/")) { + return doiTrimmed.substring(15); + } + + return doiTrimmed; + } + + return null; + } + + /** + * Label for the UI + * + * @return + */ + @Override + public String getLabel() { + return "DOI"; + } + + /** + * Resolve the DOI into other external identifiers + * + * @param externalId + * @return + */ + @Override + public ExternalIdentifiers allExternalIDsForFind(String externalId) { + // For now, just return the DOI + ExternalIdentifiers ids = new ExternalIdentifiers(); + ids.DOI = externalId; + return ids; + } + + /** + * Look up the DOI in CrossRef, and populate a citation object + * + * @param id + * @param citation + * @return + */ + @Override + public String findInExternal(String id, Citation citation) { + // Use content negotiation on the resolver API (wider variety of sources) + CrossrefResolverAPI resolverAPI = new CrossrefResolverAPI(); + String json = resolverAPI.findInExternal(id, citation); + + // If the content negotiation failed, use the CrossRef Native API + if (json == null) { + CrossrefNativeAPI nativeAPI = new CrossrefNativeAPI(); + json = nativeAPI.findInExternal(id, citation); + } + + // Return the JSON fragment + return json; + } + + /** + * Create an internmediate model of the external resource (JSON string) + * + * @param externalId + * @param externalResource + * @return + */ + @Override + public ResourceModel makeResourceModel(String externalId, String externalResource) { + // Note that the external resource may be slightly different, depending on whether it came from + // the resolver or native api + + // First, try the resolver API format to create the model + CrossrefResolverAPI resolverAPI = new CrossrefResolverAPI(); + ResourceModel resourceModel = resolverAPI.makeResourceModel(externalResource); + + // Otherwise, try the native API format to create the model + if (resourceModel == null) { + CrossrefNativeAPI nativeAPI = new CrossrefNativeAPI(); + resourceModel = nativeAPI.makeResourceModel(externalResource); + } + + // Return the created resource model + return resourceModel; + } +} diff --git a/api/src/main/java/org/vivoweb/webapp/createandlink/crossref/CrossrefNativeAPI.java b/api/src/main/java/org/vivoweb/webapp/createandlink/crossref/CrossrefNativeAPI.java new file mode 100644 index 00000000..ec5cd11f --- /dev/null +++ b/api/src/main/java/org/vivoweb/webapp/createandlink/crossref/CrossrefNativeAPI.java @@ -0,0 +1,368 @@ +/* $This file is distributed under the terms of the license in /doc/license.txt$ */ + +package org.vivoweb.webapp.createandlink.crossref; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.databind.DeserializationFeature; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.annotation.JsonDeserialize; +import edu.cornell.mannlib.vitro.webapp.utils.http.HttpClientFactory; +import edu.cornell.mannlib.vitro.webapp.web.URLEncoder; +import org.apache.commons.lang3.ArrayUtils; +import org.apache.commons.lang3.StringUtils; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.apache.http.HttpResponse; +import org.apache.http.client.HttpClient; +import org.apache.http.client.methods.HttpGet; +import org.vivoweb.webapp.createandlink.Citation; +import org.vivoweb.webapp.createandlink.CreateAndLinkUtils; +import org.vivoweb.webapp.createandlink.ResourceModel; +import org.vivoweb.webapp.createandlink.utils.HttpReader; +import org.vivoweb.webapp.createandlink.utils.StringArrayDeserializer; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.Date; +import java.util.List; + +/** + * Interface to CrossRef's native API + */ +@JsonIgnoreProperties +public class CrossrefNativeAPI { + private static final Log log = LogFactory.getLog(CrossrefNativeAPI.class); + + // API endpoint address + private static final String CROSSREF_API = "http://api.crossref.org/works/"; + + /** + * Find the DOI in CrossRef, filling the citation object + * + * @param id + * @param citation + * @return + */ + public String findInExternal(String id, Citation citation) { + // Get JSON from the CrossRef API + String json = readUrl(CROSSREF_API + URLEncoder.encode(id)); + + if (StringUtils.isEmpty(json)) { + return null; + } + + CrossrefResponse response = null; + try { + ObjectMapper objectMapper = new ObjectMapper(); + objectMapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false); + response = objectMapper.readValue(json, CrossrefResponse.class); + } catch (IOException e) { + log.error("Unable to read JSON value", e); + } + if (response == null || response.message == null) { + return null; + } + + // The CrossRef API sometimes gives a false record when the DOI deosn't exist + // So ensure that the response we got contains the DOI we asked for + if (!id.equalsIgnoreCase(response.message.DOI)) { + return null; + } + + // Map the fields from the CrossRef response to the Citation object + + citation.DOI = id; + citation.type = normalizeType(response.message.type); + + if (!ArrayUtils.isEmpty(response.message.title)) { + citation.title = response.message.title[0]; + } + + if (!ArrayUtils.isEmpty(response.message.containerTitle)) { + for (String journal : response.message.containerTitle) { + if (citation.journal == null || citation.journal.length() < journal.length()) { + citation.journal = journal; + } + } + } + + if (response.message.author != null) { + List authors = new ArrayList<>(); + for (CrossrefResponse.ResponseModel.Author author : response.message.author) { + Citation.Name citationAuthor = new Citation.Name(); + citationAuthor.name = CreateAndLinkUtils.formatAuthorString(author.family, author.given); + authors.add(citationAuthor); + } + citation.authors = authors.toArray(new Citation.Name[authors.size()]); + } + + citation.volume = response.message.volume; + citation.issue = response.message.issue; + citation.pagination = response.message.page; + if (citation.pagination == null) { + citation.pagination = response.message.articleNumber; + } + + citation.publicationYear = extractYearFromDateField(response.message.publishedPrint); + if (citation.publicationYear == null) { + citation.publicationYear = extractYearFromDateField(response.message.publishedOnline); + } + + return json; + } + + /** + * Retrieve the year from a compound date field + * + * @param date + * @return + */ + private Integer extractYearFromDateField(CrossrefResponse.ResponseModel.DateField date) { + if (date == null) { + return null; + } + + if (ArrayUtils.isEmpty(date.dateParts)) { + return null; + } + + return date.dateParts[0][0]; + } + + /** + * Create a full resource model from the external resource (JSON) + * @param externalResource + * @return + */ + public ResourceModel makeResourceModel(String externalResource) { + if (StringUtils.isEmpty(externalResource)) { + return null; + } + + CrossrefResponse response = null; + try { + ObjectMapper objectMapper = new ObjectMapper(); + objectMapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false); + response = objectMapper.readValue(externalResource, CrossrefResponse.class); + } catch (IOException e) { + log.error("Unable to read JSON", e); + } + if (response == null || response.message == null) { + return null; + } + + if (StringUtils.isEmpty(response.message.DOI)) { + return null; + } + + // Map the fields from the CrossRef response to the resource model + + ResourceModel model = new ResourceModel(); + + model.DOI = response.message.DOI; + model.ISSN = response.message.ISSN; + model.URL = response.message.URL; + + if (response.message.author != null && response.message.author.length > 0) { + model.author = new ResourceModel.NameField[response.message.author.length]; + for (int authIdx = 0; authIdx < response.message.author.length; authIdx++) { + if (response.message.author[authIdx] != null) { + model.author[authIdx] = new ResourceModel.NameField(); + model.author[authIdx].family = response.message.author[authIdx].family; + model.author[authIdx].given = response.message.author[authIdx].given; + } + } + } + + + if (response.message.containerTitle != null && response.message.containerTitle.length > 0) { + String journalName = null; + for (String container : response.message.containerTitle) { + if (journalName == null || container.length() > journalName.length()) { + journalName = container; + } + } + model.containerTitle = journalName; + } + + model.issue = response.message.issue; + + if (!StringUtils.isEmpty(response.message.page)) { + if (response.message.page.contains("-")) { + int hyphen = response.message.page.indexOf('-'); + model.pageStart = response.message.page.substring(0, hyphen); + model.pageEnd = response.message.page.substring(hyphen + 1); + } else { + model.pageStart = response.message.page; + } + } else if (!StringUtils.isEmpty(response.message.articleNumber)) { + model.pageStart = response.message.articleNumber; + } + + model.publicationDate = convertDateField(response.message.publishedPrint); + if (model.publicationDate == null) { + model.publicationDate = convertDateField(response.message.publishedOnline); + } + + model.publisher = response.message.publisher; + model.subject = response.message.subject; + if (response.message.title != null && response.message.title.length > 0) { + model.title = response.message.title[0]; + } + + model.type = normalizeType(response.message.type); + model.volume = response.message.volume; + + return model; + } + + /** + * Map non-standard publication types into the CiteProc types + * + * @param type + * @return + */ + private String normalizeType(String type) { + if (type != null) { + switch (type.toLowerCase()) { + case "journal-article": + return "article-journal"; + + case "book-chapter": + return "chapter"; + + case "proceedings-article": + return "paper-conference"; + } + } + + return type; + } + + /** + * Convert a date field from the CrossRef response to the internal resource model format + * + * @param dateField + * @return + */ + private ResourceModel.DateField convertDateField(CrossrefResponse.ResponseModel.DateField dateField) { + if (dateField != null) { + ResourceModel.DateField resourceDate = new ResourceModel.DateField(); + if (dateField.dateParts != null && dateField.dateParts.length > 0 && dateField.dateParts[0].length > 0) { + if (dateField.dateParts.length == 1) { + resourceDate.year = dateField.dateParts[0][0]; + } else if (dateField.dateParts.length == 2) { + resourceDate.year = dateField.dateParts[0][0]; + resourceDate.month = dateField.dateParts[0][1]; + } else { + resourceDate.year = dateField.dateParts[0][0]; + resourceDate.month = dateField.dateParts[0][1]; + resourceDate.day = dateField.dateParts[0][2]; + } + } + return resourceDate; + } + + return null; + } + + /** + * Read JSON from the given URL + * + * @param url + * @return + */ + private String readUrl(String url) { + try { + HttpClient client = HttpClientFactory.getHttpClient(); + HttpGet request = new HttpGet(url); + HttpResponse response = client.execute(request); + return HttpReader.fromResponse(response); + } catch (IOException e) { + } + + return null; + } + + /** + * Java object representation of the JSON returned by CrossRef + */ + private static class CrossrefResponse { + public ResponseModel message; + + @JsonProperty("message-type") + public String messageType; + + @JsonProperty("message-version") + public String messageVersion; + + public String status; + + public static class ResponseModel { + public String DOI; + @JsonDeserialize(using = StringArrayDeserializer.class) + public String[] ISSN; + public String URL; + + @JsonProperty("alternative-id") + @JsonDeserialize(using = StringArrayDeserializer.class) + public String[] alternativeId; + + public Author[] author; + + @JsonProperty("container-title") + @JsonDeserialize(using = StringArrayDeserializer.class) + public String[] containerTitle; + public DateField created; + public DateField deposited; + public DateField indexed; + public String issue; + public DateField issued; + public String member; + public String page; + public String prefix; + + @JsonProperty("article-number") + public String articleNumber; + + @JsonProperty("published-online") + public DateField publishedOnline; + + @JsonProperty("published-print") + public DateField publishedPrint; + + public String publisher; + + @JsonProperty("reference-count") + public Integer referenceCount; + public Double score; + @JsonDeserialize(using = StringArrayDeserializer.class) + public String[] subject; + @JsonDeserialize(using = StringArrayDeserializer.class) + public String[] subtitle; + @JsonDeserialize(using = StringArrayDeserializer.class) + public String[] title; + public String type; + public String volume; + + + public static class Author { + @JsonDeserialize(using = StringArrayDeserializer.class) + public String[] affiliation; + public String family; + public String given; + } + + public static class DateField { + @JsonProperty("date-parts") + public Integer[][] dateParts; + + @JsonProperty("date-time") + public Date dateTime; + + public Long timestamp; + } + } + } +} diff --git a/api/src/main/java/org/vivoweb/webapp/createandlink/crossref/CrossrefResolverAPI.java b/api/src/main/java/org/vivoweb/webapp/createandlink/crossref/CrossrefResolverAPI.java new file mode 100644 index 00000000..7fb949ab --- /dev/null +++ b/api/src/main/java/org/vivoweb/webapp/createandlink/crossref/CrossrefResolverAPI.java @@ -0,0 +1,392 @@ +/* $This file is distributed under the terms of the license in /doc/license.txt$ */ + +package org.vivoweb.webapp.createandlink.crossref; + +import com.fasterxml.jackson.databind.DeserializationFeature; +import com.fasterxml.jackson.databind.ObjectMapper; +import edu.cornell.mannlib.vitro.webapp.utils.http.HttpClientFactory; +import edu.cornell.mannlib.vitro.webapp.web.URLEncoder; +import org.apache.commons.lang3.ArrayUtils; +import org.apache.commons.lang3.StringUtils; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.apache.http.HttpResponse; +import org.apache.http.client.HttpClient; +import org.apache.http.client.methods.HttpGet; +import org.vivoweb.webapp.createandlink.Citation; +import org.vivoweb.webapp.createandlink.CreateAndLinkUtils; +import org.vivoweb.webapp.createandlink.ResourceModel; +import org.vivoweb.webapp.createandlink.utils.HttpReader; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.List; + +/** + * Interface to the CrossRef resolver + */ +public class CrossrefResolverAPI { + protected final Log logger = LogFactory.getLog(getClass()); + + // Base URL for the resolver + private static final String CROSSREF_RESOLVER = "https://doi.org/"; + + /** + * Find the DOI in CrossRef, filling the citation object + * + * @param id + * @param citation + * @return + */ + public String findInExternal(String id, Citation citation) { + try { + // Read JSON from the resolver + String json = readJSON(CROSSREF_RESOLVER + URLEncoder.encode(id)); + + if (StringUtils.isEmpty(json)) { + return null; + } + + ObjectMapper objectMapper = new ObjectMapper(); + objectMapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false); + CrossrefCiteprocJSONModel jsonModel = objectMapper.readValue(json, CrossrefCiteprocJSONModel.class); + if (jsonModel == null) { + return null; + } + + // Ensure that we have the correct resource + if (!id.equalsIgnoreCase(jsonModel.DOI)) { + return null; + } + + // Map the fields of the resolver response to the citation object + + citation.DOI = id; + citation.type = normalizeType(jsonModel.type); + citation.title = jsonModel.title; + citation.journal = jsonModel.containerTitle; + + if (jsonModel.author != null) { + List authors = new ArrayList<>(); + for (CrossrefCiteprocJSONModel.NameField author : jsonModel.author) { + splitNameLiteral(author); + Citation.Name citationAuthor = new Citation.Name(); + citationAuthor.name = CreateAndLinkUtils.formatAuthorString(author.family, author.given); + authors.add(citationAuthor); + } + citation.authors = authors.toArray(new Citation.Name[authors.size()]); + } + + citation.volume = jsonModel.volume; + citation.issue = jsonModel.issue; + citation.pagination = jsonModel.page; + if (citation.pagination == null) { + citation.pagination = jsonModel.articleNumber; + } + + citation.publicationYear = extractYearFromDateField(jsonModel.publishedPrint); + if (citation.publicationYear == null) { + citation.publicationYear = extractYearFromDateField(jsonModel.publishedOnline); + } + + return json; + } catch (Exception e) { + logger.error("[CREF] Error resolving DOI " + id + ", cause "+ e.getMessage()); + return null; + } + } + + /** + * Extract the year from the crossref JSON model + * + * @param date + * @return + */ + private Integer extractYearFromDateField(CrossrefCiteprocJSONModel.DateField date) { + if (date == null) { + return null; + } + + if (ArrayUtils.isEmpty(date.dateParts)) { + return null; + } + + return Integer.parseInt(date.dateParts[0][0]); + } + + /** + * + * @param externalResource + * @return + */ + public ResourceModel makeResourceModel(String externalResource) { + if (StringUtils.isEmpty(externalResource)) { + return null; + } + + CrossrefCiteprocJSONModel jsonModel = null; + try { + ObjectMapper objectMapper = new ObjectMapper(); + objectMapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false); + jsonModel = objectMapper.readValue(externalResource, CrossrefCiteprocJSONModel.class); + } catch (IOException e) { + logger.error("Unable to read JSON", e); + } + if (jsonModel == null) { + return null; + } + + if (StringUtils.isEmpty(jsonModel.DOI)) { + return null; + } + + // Map the fields of the Java object to the resource model + + ResourceModel model = new ResourceModel(); + + model.DOI = jsonModel.DOI; + model.PubMedID = jsonModel.PMID; + model.PubMedCentralID = jsonModel.PMCID; + model.ISSN = jsonModel.ISSN; + model.ISBN = jsonModel.ISBN; + model.URL = jsonModel.URL; + + if (jsonModel.ISBN != null) { + int isbnIdx = 0; + model.ISBN = new String[jsonModel.ISBN.length]; + for (String isbn : jsonModel.ISBN) { + if (isbn.lastIndexOf('/') > -1) { + isbn = isbn.substring(isbn.lastIndexOf('/') + 1); + } + + model.ISBN[isbnIdx] = isbn; + isbnIdx++; + } + } + + model.author = convertNameFields(jsonModel.author); + model.editor = convertNameFields(jsonModel.editor); + model.translator = convertNameFields(jsonModel.translator); + + model.containerTitle = jsonModel.containerTitle; + + model.issue = jsonModel.issue; + + if (!StringUtils.isEmpty(jsonModel.page)) { + if (jsonModel.page.contains("-")) { + int hyphen = jsonModel.page.indexOf('-'); + model.pageStart = jsonModel.page.substring(0, hyphen); + model.pageEnd = jsonModel.page.substring(hyphen + 1); + } else { + model.pageStart = jsonModel.page; + } + } else if (!StringUtils.isEmpty(jsonModel.articleNumber)) { + model.pageStart = jsonModel.articleNumber; + } + + model.publicationDate = convertDateField(jsonModel.publishedPrint); + if (model.publicationDate == null) { + model.publicationDate = convertDateField(jsonModel.publishedOnline); + } + + model.publisher = jsonModel.publisher; + model.subject = jsonModel.subject; + model.title = jsonModel.title; + model.type = normalizeType(jsonModel.type); + model.volume = jsonModel.volume; + + model.status = jsonModel.status; + model.presentedAt = jsonModel.event; + model.abstractText = jsonModel.abstractText; + + return model; + } + + /** + * Convert CiteProc name fields into resource model name fields + * + * @param nameFields + * @return + */ + private ResourceModel.NameField[] convertNameFields(CrossrefCiteprocJSONModel.NameField[] nameFields) { + if (nameFields == null) { + return null; + } + + ResourceModel.NameField[] destNameFields = new ResourceModel.NameField[nameFields.length]; + + for (int nameIdx = 0; nameIdx < nameFields.length; nameIdx++) { + if (nameFields[nameIdx] != null) { + splitNameLiteral(nameFields[nameIdx]); + destNameFields[nameIdx] = new ResourceModel.NameField(); + destNameFields[nameIdx].family = nameFields[nameIdx].family; + destNameFields[nameIdx].given = nameFields[nameIdx].given; + } + } + + return destNameFields; + } + + /** + * Map non-standard publication types into the CiteProc types + * + * @param type + * @return + */ + private String normalizeType(String type) { + if (type != null) { + switch (type.toLowerCase()) { + case "journal-article": + return "article-journal"; + + case "book-chapter": + return "chapter"; + + case "proceedings-article": + return "paper-conference"; + } + } + + return type; + } + + /** + * Split a name literal into first and last names + * + * @param author + */ + private void splitNameLiteral(CrossrefCiteprocJSONModel.NameField author) { + if (StringUtils.isEmpty(author.family)) { + String given = null; + if (!StringUtils.isEmpty(author.literal)) { + if (author.literal.contains(",")) { + author.family = author.literal.substring(0, author.literal.indexOf(',')); + given = author.literal.substring(author.literal.indexOf(',') + 1); + } else if (author.literal.lastIndexOf(' ') > -1) { + author.family = author.literal.substring(author.literal.lastIndexOf(' ') + 1); + given = author.literal.substring(0, author.literal.lastIndexOf(' ')); + } else { + author.family = author.literal; + } + } + + if (StringUtils.isEmpty(author.given)) { + author.given = given; + } + } + } + + /** + * Convert a CiteProc date field to resource model date field + * + * @param dateField + * @return + */ + private ResourceModel.DateField convertDateField(CrossrefCiteprocJSONModel.DateField dateField) { + if (dateField != null) { + ResourceModel.DateField resourceDate = new ResourceModel.DateField(); + if (dateField.dateParts != null && dateField.dateParts.length > 0 && dateField.dateParts[0].length > 0) { + try { + resourceDate.year = Integer.parseInt(dateField.dateParts[0][0], 10); + } catch (NumberFormatException nfe) { + } + if (dateField.dateParts.length > 1) { + try { + resourceDate.month = Integer.parseInt(dateField.dateParts[0][1], 10); + } catch (NumberFormatException nfe) { + switch (dateField.dateParts[0][1].toLowerCase()) { + case "jan": + case "january": + resourceDate.month = 1; + break; + + case "feb": + case "february": + resourceDate.month = 2; + break; + + case "mar": + case "march": + resourceDate.month = 3; + break; + + case "apr": + case "april": + resourceDate.month = 4; + break; + + case "may": + resourceDate.month = 5; + break; + + case "jun": + case "june": + resourceDate.month = 6; + break; + + case "jul": + case "july": + resourceDate.month = 7; + break; + + case "aug": + case "august": + resourceDate.month = 8; + break; + + case "sep": + case "september": + resourceDate.month = 9; + break; + + case "oct": + case "october": + resourceDate.month = 10; + break; + + case "nov": + case "november": + resourceDate.month = 11; + break; + + case "dec": + case "december": + resourceDate.month = 12; + break; + } + } + } + if (dateField.dateParts.length > 2) { + try { + resourceDate.day = Integer.parseInt(dateField.dateParts[0][2], 10); + } catch (NumberFormatException nfe) { + } + } + } + return resourceDate; + } + + return null; + } + + /** + * Read JSON from the URL + * @param url + * @return + */ + private String readJSON(String url) { + try { + HttpClient client = HttpClientFactory.getHttpClient(); + HttpGet request = new HttpGet(url); + + // Content negotiate for csl / citeproc JSON + request.setHeader("Accept", "application/vnd.citationstyles.csl+json;q=1.0"); + + HttpResponse response = client.execute(request); + return HttpReader.fromResponse(response); + } catch (IOException e) { + } + + return null; + } +} diff --git a/api/src/main/java/org/vivoweb/webapp/createandlink/pubmed/PubMedCreateAndLinkResourceProvider.java b/api/src/main/java/org/vivoweb/webapp/createandlink/pubmed/PubMedCreateAndLinkResourceProvider.java new file mode 100644 index 00000000..4e164c12 --- /dev/null +++ b/api/src/main/java/org/vivoweb/webapp/createandlink/pubmed/PubMedCreateAndLinkResourceProvider.java @@ -0,0 +1,377 @@ +/* $This file is distributed under the terms of the license in /doc/license.txt$ */ + +package org.vivoweb.webapp.createandlink.pubmed; + +import com.fasterxml.jackson.core.JsonFactory; +import com.fasterxml.jackson.core.JsonParser; +import com.fasterxml.jackson.core.JsonToken; +import com.fasterxml.jackson.databind.DeserializationFeature; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.annotation.JsonDeserialize; +import edu.cornell.mannlib.vitro.webapp.utils.http.HttpClientFactory; +import org.apache.commons.lang3.ArrayUtils; +import org.apache.commons.lang3.StringUtils; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.apache.http.HttpResponse; +import org.apache.http.client.HttpClient; +import org.apache.http.client.methods.HttpGet; +import org.vivoweb.webapp.createandlink.Citation; +import org.vivoweb.webapp.createandlink.CreateAndLinkResourceProvider; +import org.vivoweb.webapp.createandlink.ExternalIdentifiers; +import org.vivoweb.webapp.createandlink.ResourceModel; +import org.vivoweb.webapp.createandlink.utils.HttpReader; +import org.vivoweb.webapp.createandlink.utils.StringArrayDeserializer; + +import java.io.IOException; + +public class PubMedCreateAndLinkResourceProvider implements CreateAndLinkResourceProvider { + protected final Log logger = LogFactory.getLog(getClass()); + + public final static String PUBMED_ID_API = "http://www.ncbi.nlm.nih.gov/pmc/utils/idconv/v1.0/?format=json&ids="; + public final static String PUBMED_SUMMARY_API = "http://eutils.ncbi.nlm.nih.gov/entrez/eutils/esummary.fcgi?db=pubmed&retmode=json&tool=my_tool&email=my_email@example.com&id="; + + @Override + public String normalize(String id) { + return id.trim(); + } + + @Override + public String getLabel() { + return "PubMed ID"; + } + + @Override + public ExternalIdentifiers allExternalIDsForFind(String externalId) { + ExternalIdentifiers ids = new ExternalIdentifiers(); + ids.PubMedID = externalId; + + String json = readUrl(PUBMED_ID_API + externalId); + if (!StringUtils.isEmpty(json)) { + PubMedIDResponse response = null; + try { + ObjectMapper objectMapper = new ObjectMapper(); + objectMapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false); + response = objectMapper.readValue(json, PubMedIDResponse.class); + } catch (IOException e) { + logger.error("Unable to read JSON", e); + } + if (response != null && !ArrayUtils.isEmpty(response.records)) { + ids.DOI = response.records[0].doi; + ids.PubMedCentralID = response.records[0].pmcid; + } + } + + return ids; + } + + @Override + public String findInExternal(String id, Citation citation) { + try { + String json = readUrl(PUBMED_SUMMARY_API + id); + if (StringUtils.isEmpty(json)) { + return null; + } + + JsonFactory factory = new JsonFactory(); + JsonParser parser = factory.createParser(json); + if (parser != null) { + while (!parser.isClosed() && !id.equals(parser.getCurrentName())) { + JsonToken token = parser.nextToken(); + } + + if (!parser.isClosed()) { + // We have reached the field for our ID, but we need to be on the next token for the mapper to work + JsonToken token = parser.nextToken(); + ObjectMapper objectMapper = new ObjectMapper(); + objectMapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false); + PubMedSummaryResponse response = objectMapper.readValue(parser, PubMedSummaryResponse.class); + if (response != null) { + citation.title = response.title; + citation.authors = new Citation.Name[response.authors.length]; + for (int idx = 0; idx < response.authors.length; idx++) { + citation.authors[idx] = new Citation.Name(); + citation.authors[idx].name = normalizeAuthorName(response.authors[idx].name); + } + citation.journal = response.fulljournalname; + citation.volume = response.volume; + citation.issue = response.issue; + citation.pagination = response.pages; + if (!StringUtils.isEmpty(response.pubdate) && response.pubdate.length() >= 4) { + citation.publicationYear = Integer.parseInt(response.pubdate.substring(0, 4), 10); + } + + citation.type = getCiteprocTypeForPubType(response.pubtype); + + return json; + } + } + } + + return null; + } catch (Exception e) { + logger.error("[PMID] Error resolving PMID " + id + ", cause "+ e.getMessage()); + return null; + } + } + + @Override + public ResourceModel makeResourceModel(String externalId, String externalResource) { + try { + JsonFactory factory = new JsonFactory(); + JsonParser parser = factory.createParser(externalResource); + if (parser != null) { + while (!parser.isClosed() && !externalId.equals(parser.getCurrentName())) { + JsonToken token = parser.nextToken(); + } + + if (!parser.isClosed()) { + // We have reached the field for our ID, but we need to be on the next token for the mapper to work + JsonToken token = parser.nextToken(); + ObjectMapper objectMapper = new ObjectMapper(); + objectMapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false); + PubMedSummaryResponse response = objectMapper.readValue(parser, PubMedSummaryResponse.class); + if (response != null) { + ResourceModel resourceModel = new ResourceModel(); + resourceModel.PubMedID = externalId; + resourceModel.title = response.title; + resourceModel.author = new ResourceModel.NameField[response.authors.length]; + for (int idx = 0; idx < response.authors.length; idx++) { + resourceModel.author[idx] = new ResourceModel.NameField(); + if (response.authors[idx].name.lastIndexOf(' ') > 0) { + resourceModel.author[idx].family = response.authors[idx].name.substring(0, response.authors[idx].name.lastIndexOf(' ')); + resourceModel.author[idx].given = response.authors[idx].name.substring(response.authors[idx].name.lastIndexOf(' ') + 1); + } else { + resourceModel.author[idx].family = response.authors[idx].name; + } + } + + resourceModel.containerTitle = response.fulljournalname; + if (!StringUtils.isEmpty(response.issn)) { + resourceModel.ISSN = new String[1]; + resourceModel.ISSN[0] = response.issn; + } else if (!StringUtils.isEmpty(response.eissn)) { + resourceModel.ISSN = new String[1]; + resourceModel.ISSN[0] = response.eissn; + } + + resourceModel.volume = response.volume; + resourceModel.issue = response.issue; + if (response.pages.contains("-")) { + int hyphen = response.pages.indexOf('-'); + resourceModel.pageStart = response.pages.substring(0, hyphen); + resourceModel.pageEnd = response.pages.substring(hyphen + 1); + } else { + resourceModel.pageStart = response.pages; + } + + if (!StringUtils.isEmpty(response.pubdate) && response.pubdate.length() >= 4) { + resourceModel.publicationDate = new ResourceModel.DateField(); + resourceModel.publicationDate.year = Integer.parseInt(response.pubdate.substring(0, 4), 10); + } + + if (response.articleids != null) { + for (PubMedSummaryResponse.ArticleID articleID : response.articleids) { + if (!StringUtils.isEmpty(articleID.value)) { + if ("doi".equalsIgnoreCase(articleID.idtype)) { + resourceModel.DOI = articleID.value.trim(); + } else if ("pmc".equalsIgnoreCase(articleID.idtype)) { + resourceModel.PubMedCentralID = articleID.value.trim(); + } else if ("pmcid".equalsIgnoreCase(articleID.idtype)) { + if (StringUtils.isEmpty(resourceModel.PubMedCentralID)) { + String id = articleID.value.replaceAll(".*(PMC[0-9]+).*", "$1"); + if (!StringUtils.isEmpty(id)) { + resourceModel.PubMedCentralID = id; + } + } + } + } + } + } + + resourceModel.type = getCiteprocTypeForPubType(response.pubtype); + resourceModel.publisher = response.publishername; + resourceModel.status = response.pubstatus; + + /* + public DateField created; + public String[] subject; + public String presentedAt; + public String[] keyword; + public String abstractText; + */ + return resourceModel; + } + } + } + } catch (IOException e) { + logger.error("Unable to read JSON", e); + } + + return null; + } + + private String normalizeAuthorName(String name) { + if (name.indexOf(',') < 0 && name.indexOf(' ') > -1) { + int lastSpace = name.lastIndexOf(' '); + int insertPoint = lastSpace; + while (insertPoint > 0) { + if (name.charAt(insertPoint - 1) == ' ') { + insertPoint--; + } else { + break; + } + } + + return name.substring(0, insertPoint) + "," + name.substring(lastSpace); + } + return name; + } + + private String getCiteprocTypeForPubType(String[] pubTypes) { + if (pubTypes != null && pubTypes.length > 0) { + for (String pubType : pubTypes) { + switch (pubType) { + case "Journal Article": + return "article-journal"; + + case "Incunabula": + case "Monograph": + case "Textbooks": + return "book"; + + case "Dataset": + return "dataset"; + + case "Legal Cases": + return "legal_case"; + + case "Legislation": + return "legislation"; + + case "Manuscripts": + return "manuscript"; + + case "Maps": + return "map"; + + case "Meeting Abstracts": + return "paper-conference"; + + case "Patents": + return "patent"; + + case "Letter": + return "personal_communication"; + + case "Blogs": + return "post-weblog"; + + case "Review": + return "review"; + + case "Academic Dissertations": + return "thesis"; + } + } + } + + return "article-journal"; + } + + private String readUrl(String url) { + try { + HttpClient client = HttpClientFactory.getHttpClient(); + HttpGet request = new HttpGet(url); + HttpResponse response = client.execute(request); + return HttpReader.fromResponse(response); + } catch (IOException e) { + } + + return null; + } + + private static class PubMedIDResponse { + public String status; + public String responseDate; + public String request; + public String warning; + + public PubMedIDRecord[] records; + + public static class PubMedIDRecord { + String pmcid; + String pmid; + String doi; + + // Don't need versions + } + } + + private static class PubMedSummaryResponse { + public String uid; + public String pubdate; + //public String epubdate; + public String source; + public NameField[] authors; + //public String lastauthor; + public String title; + //public String sorttitle; + public String volume; + public String issue; + public String pages; + @JsonDeserialize(using = StringArrayDeserializer.class) + public String[] lang; + //public String nlmuniqueid; + public String issn; + public String eissn; + @JsonDeserialize(using = StringArrayDeserializer.class) + public String[] pubtype; + //public String recordstatus; + public String pubstatus; + public ArticleID[] articleids; + public History[] history; + //public String[] references; + @JsonDeserialize(using = StringArrayDeserializer.class) + public String[] attributes; + //public Integer pmcrefcount; + public String fulljournalname; + //public String elocationid; + //public Integer viewcount; + //public String doctype; + //public String[] srccontriblist; + //public String booktitle; + //public String medium; + //public String edition; + //public String publisherlocation; + public String publishername; + //public String srcdate; + //public String reportnumber; + //public String availablefromurl; + //public String locationlabel; + //public String[] doccontriblist; + //public String docdate; + //public String bookname; + public String chapter; + //public String sortpubdate; + //public String sortfirstauthor; + //public String vernaculartitle; + + public static class NameField { + public String name; + //public String authtype; + //public String clusterid; + } + + public static class ArticleID { + public String idtype; + //public Integer idtypen; + public String value; + } + + public static class History { + public String pubstatus; + public String date; + } + } +} diff --git a/api/src/main/java/org/vivoweb/webapp/createandlink/utils/HttpReader.java b/api/src/main/java/org/vivoweb/webapp/createandlink/utils/HttpReader.java new file mode 100644 index 00000000..2a274f0c --- /dev/null +++ b/api/src/main/java/org/vivoweb/webapp/createandlink/utils/HttpReader.java @@ -0,0 +1,35 @@ +/* $This file is distributed under the terms of the license in /doc/license.txt$ */ + +package org.vivoweb.webapp.createandlink.utils; + +import org.apache.commons.io.IOUtils; +import org.apache.http.HttpEntity; +import org.apache.http.HttpResponse; +import org.apache.http.util.EntityUtils; + +import java.io.IOException; +import java.io.InputStream; +import java.io.StringWriter; + +public class HttpReader { + public static String fromResponse(HttpResponse response) throws IOException { + HttpEntity entity = response != null ? response.getEntity() : null; + try { + if (entity != null) { + if (response.getStatusLine().getStatusCode() == 200) { + try (InputStream in = entity.getContent()) { + StringWriter writer = new StringWriter(); + IOUtils.copy(in, writer, "UTF-8"); + return writer.toString(); + } + } + } + } finally { + if (entity != null) { + EntityUtils.consume(entity); + } + } + + return null; + } +} diff --git a/api/src/main/java/org/vivoweb/webapp/createandlink/utils/StringArrayDeserializer.java b/api/src/main/java/org/vivoweb/webapp/createandlink/utils/StringArrayDeserializer.java new file mode 100644 index 00000000..5354d03a --- /dev/null +++ b/api/src/main/java/org/vivoweb/webapp/createandlink/utils/StringArrayDeserializer.java @@ -0,0 +1,35 @@ +/* $This file is distributed under the terms of the license in /doc/license.txt$ */ + +package org.vivoweb.webapp.createandlink.utils; + +import com.fasterxml.jackson.core.JsonParser; +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.core.JsonToken; +import com.fasterxml.jackson.databind.DeserializationContext; +import com.fasterxml.jackson.databind.JsonDeserializer; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.List; + +public class StringArrayDeserializer extends JsonDeserializer { + @Override + public String[] deserialize(JsonParser jsonParser, DeserializationContext context) throws IOException, JsonProcessingException { + if (JsonToken.VALUE_NULL.equals(jsonParser.getCurrentToken())) { + jsonParser.nextToken(); + return null; + } + + if (JsonToken.START_ARRAY.equals(jsonParser.getCurrentToken())) { + List list = new ArrayList<>(); + while (!JsonToken.END_ARRAY.equals(jsonParser.nextToken())) { + list.add(jsonParser.getValueAsString()); + } + return list.toArray(new String[list.size()]); + } else if (JsonToken.VALUE_STRING.equals(jsonParser.getCurrentToken())) { + return new String[] { jsonParser.getText() }; + } + + return null; + } +} diff --git a/home/src/main/resources/config/example.runtime.properties b/home/src/main/resources/config/example.runtime.properties index af7459a3..87fc9621 100644 --- a/home/src/main/resources/config/example.runtime.properties +++ b/home/src/main/resources/config/example.runtime.properties @@ -375,8 +375,14 @@ Vitro.reconcile.defaultTypeList = http://vivoweb.org/ontology/core#Role, core:Ro http://xmlns.com/foaf/0.1/Person, foaf:Person; \ http://purl.obolibrary.org/obo/IAO_0000030, obo:IAO_0000030 + # Configure the support for claiming by DOI or PMID + # This is a list of all the providers that are active for claiming articles from + # Options: doi, pmid + # which search Crossref and PubMed, respectively + # If you do not wish to use the claiming interface, set this property to nothing (empty) +createAndLink.providers = doi, pmid -# Triple pattern fragments is a very fast, very simple means for querying a triple store. -# The triple pattern fragments API in VIVO puts little load on the server, providing a simple means for getting data from the triple store. The API has a web interface for manual use, can be used from the command line via curl, and can be used by programs. - -# tpf.activeFlag = true + # Triple pattern fragments is a very fast, very simple means for querying a triple store. + # The triple pattern fragments API in VIVO puts little load on the server, providing a simple means for getting data from the triple store. The API has a web interface for manual use, can be used from the command line via curl, and can be used by programs. + # +# tpf.activeFlag = true \ No newline at end of file diff --git a/webapp/src/main/webapp/i18n/vivo_all.properties b/webapp/src/main/webapp/i18n/vivo_all.properties index d54ceaf0..08a11663 100644 --- a/webapp/src/main/webapp/i18n/vivo_all.properties +++ b/webapp/src/main/webapp/i18n/vivo_all.properties @@ -810,3 +810,55 @@ role_in_presentation_capitalized=Role in Presentation advisee_capitalized_first_name=First Name advisee_capitalized_lastname=Last Name +# Messages for creating and linking resources (publications) +create_and_link_enter=Enter {0}: +create_and_link_claim_for=Claiming works for
{0} +create_and_link_confirm_works=Confirm your work(s) +create_and_link_confirm_works_intro=Please check that these are the work(s) that you wish to claim, and indicate your relationship with them. +create_and_link_authors=Authors +create_and_link_authors_desc=If you are an author of a work, please select your name in the author list.
Retrieved metadata may be incomplete. If you can not see your name listed, select "Unlisted Author". +create_and_link_editors=Editors +create_and_link_editors_desc=If you edited the work, please select "Editor". +create_and_link_not_mine_desc=If you do not wish to claim a work, select "This is not my work". +create_and_link_already_claimed=You have already claimed this work. +create_and_link_unlisted_author=Unlisted Author +create_and_link_editor=Editor +create_and_link_not_mine=This is not my work +create_and_link_remaining=There are {0} ids remaining +create_and_link_thank_you=Thank you +create_and_link_finished=There are no more works left to claim.
You may enter more IDs below, or view your profile. +create_and_link_go_profile=Go to profile +create_and_link_enter_dois_intro=You may enter one or more DOIs to match, and can be entered either as an ID or URL:

e.g. +create_and_link_enter_dois_supported=Currently, DOIs issued by Crossref, DataCite and mEDRA are supported.
Each DOI should be separated by a comma or new line. +create_and_link_enter_pmid_intro=You may enter one or more PubMed IDs to match. Each ID should be separated by a comma or new line. +create_and_link_enter_pmid_supported=Note that metadata will be retrieved from Crossref, if the PubMed ID can be resolved to a DOI. +create_and_link_unknown_profile=Unknown Profile +create_and_link_unknown_resource=Unknown Resource Type +create_and_link_unauthorized_for_profile=You do not have permissions to claim for this user +create_and_link_submit_ids=Submit IDs +create_and_link_submit_confirm=Confirm +create_and_link_error=Unable to retrieve citation details +create_and_link_type_article=Article +create_and_link_type_article_journal=Journal Article +create_and_link_type_book=Book +create_and_link_type_chapter=Chapter +create_and_link_type_dataset=Dataset +create_and_link_type_figure=Image +create_and_link_type_graphic=Image +create_and_link_type_legal_case=Legal Case +create_and_link_type_legislation=Legislation +create_and_link_type_manuscript=Manuscript +create_and_link_type_map=Map +create_and_link_type_musical_score=Musical Score +create_and_link_type_paper_conference=Conference Paper +create_and_link_type_patent=Patent +create_and_link_type_personal_communication=Letter +create_and_link_type_post_weblog=Blog +create_and_link_type_report=Report +create_and_link_type_review=Review +create_and_link_type_speech=Speech +create_and_link_type_thesis=Thesis +create_and_link_type_webpage=Webpage +claim_publications_by=Claim publications by +claim_publications_by_doi=DOI +claim_publications_by_pmid=PubMed ID \ No newline at end of file diff --git a/webapp/src/main/webapp/images/createAndLink/error.png b/webapp/src/main/webapp/images/createAndLink/error.png new file mode 100644 index 00000000..79bc4d75 Binary files /dev/null and b/webapp/src/main/webapp/images/createAndLink/error.png differ diff --git a/webapp/src/main/webapp/images/createAndLink/tick.png b/webapp/src/main/webapp/images/createAndLink/tick.png new file mode 100644 index 00000000..7ceffa38 Binary files /dev/null and b/webapp/src/main/webapp/images/createAndLink/tick.png differ diff --git a/webapp/src/main/webapp/templates/freemarker/body/createAndLinkResource/createAndLinkResourceConfirm.ftl b/webapp/src/main/webapp/templates/freemarker/body/createAndLinkResource/createAndLinkResourceConfirm.ftl new file mode 100644 index 00000000..cb5e9e78 --- /dev/null +++ b/webapp/src/main/webapp/templates/freemarker/body/createAndLinkResource/createAndLinkResourceConfirm.ftl @@ -0,0 +1,111 @@ +<#setting number_format="computer"> + diff --git a/webapp/src/main/webapp/templates/freemarker/body/createAndLinkResource/createAndLinkResourceEnterID.ftl b/webapp/src/main/webapp/templates/freemarker/body/createAndLinkResource/createAndLinkResourceEnterID.ftl new file mode 100644 index 00000000..eca21dd8 --- /dev/null +++ b/webapp/src/main/webapp/templates/freemarker/body/createAndLinkResource/createAndLinkResourceEnterID.ftl @@ -0,0 +1,35 @@ + diff --git a/webapp/src/main/webapp/templates/freemarker/body/createAndLinkResource/unauthorizedForProfile.ftl b/webapp/src/main/webapp/templates/freemarker/body/createAndLinkResource/unauthorizedForProfile.ftl new file mode 100644 index 00000000..93bf5608 --- /dev/null +++ b/webapp/src/main/webapp/templates/freemarker/body/createAndLinkResource/unauthorizedForProfile.ftl @@ -0,0 +1 @@ +

${i18n().create_and_link_unauthorized_for_profile}

diff --git a/webapp/src/main/webapp/templates/freemarker/body/createAndLinkResource/unknownProfile.ftl b/webapp/src/main/webapp/templates/freemarker/body/createAndLinkResource/unknownProfile.ftl new file mode 100644 index 00000000..107231d0 --- /dev/null +++ b/webapp/src/main/webapp/templates/freemarker/body/createAndLinkResource/unknownProfile.ftl @@ -0,0 +1,2 @@ +

${i18n().create_and_link_unknown_profile}

+ diff --git a/webapp/src/main/webapp/templates/freemarker/body/createAndLinkResource/unknownResourceType.ftl b/webapp/src/main/webapp/templates/freemarker/body/createAndLinkResource/unknownResourceType.ftl new file mode 100644 index 00000000..417a5195 --- /dev/null +++ b/webapp/src/main/webapp/templates/freemarker/body/createAndLinkResource/unknownResourceType.ftl @@ -0,0 +1,2 @@ +

${i18n().create_and_link_unknown_resource}

+ diff --git a/webapp/src/main/webapp/themes/tenderfoot/css/page-createAndLink.css b/webapp/src/main/webapp/themes/tenderfoot/css/page-createAndLink.css new file mode 100644 index 00000000..f71cee1c --- /dev/null +++ b/webapp/src/main/webapp/themes/tenderfoot/css/page-createAndLink.css @@ -0,0 +1,82 @@ +#createAndLink select { + height: 2.5em; + margin-top: 0px; + margin-bottom: 0px; + padding-bottom: 0px; + padding-top: 0px; +} + +#createAndLink .citation_error:before { + content: url('../../../images/createAndLink/error.png'); + transform: scale(0.5); + margin-top: 10px; + margin-right: 10px; + float: left; +} +#createAndLink .citation_error { + height: 70px; +} +#createAndLink .citation_claimed:before { + content: url('../../../images/createAndLink/tick.png'); + transform: scale(0.75); + margin-top: -17px; + margin-left: 535px; + float: left; + position: absolute; +} +#createAndLink .citation_claimed:hover:before { + opacity: 0.2; +} +#createAndLink .citation_claimed .citation { + opacity: 0.2; +} +#createAndLink .citation_claimed:hover .citation { + opacity: 1.0; +} +#createAndLink .citation_type { + font-style: italic; + padding: 5px; +} +#createAndLink .citation_title { + font-weight: bold; +} +#createAndLink .citation_journal { + font-style: italic; +} +#createAndLink .claimed { + font-weight: bold; +} +#createAndLink .linked { + font-style: italic; +} +#createAndLink .entryId { + background-color: #3e8baa; /* #E0E0E0; */ + color: #ffffff; + padding: 5px; + font-weight: bold; + display: inline-block; +} +#createAndLink .entry { + border: 2px solid #3e8baa; /* #E0E0E0; */ + padding: 5px; +} +#createAndLink label { + display: inline; +} +#createAndLink .radioWithLabel:checked + .labelForRadio { + font-weight: bold; +} +#createAndLink .description { + padding-left: 22px; +} +#createAndLink .remainder { + font-style: italic; +} +#createAndLink .claim-for { + float: right; + border: 2px solid #3e8baa; /* #E0E0E0; */ + padding: 5px; +} +#createAndLink .claim-for h3 { + text-align: center; +} diff --git a/webapp/src/main/webapp/themes/tenderfoot/css/screen.css b/webapp/src/main/webapp/themes/tenderfoot/css/screen.css index 0825d7c6..0cdad6ec 100644 --- a/webapp/src/main/webapp/themes/tenderfoot/css/screen.css +++ b/webapp/src/main/webapp/themes/tenderfoot/css/screen.css @@ -30,5 +30,6 @@ VIVO tenderfoot theme: screen styles @import url("page-individual.css"); @import url("page-login.css"); @import url("page-menu.css"); +@import url("page-createAndLink.css"); @import url("https://fonts.googleapis.com/css?family=Noto+Sans"); @import url("../../../local/css/local.css"); diff --git a/webapp/src/main/webapp/themes/tenderfoot/templates/body/individual/individual--foaf-person.ftl b/webapp/src/main/webapp/themes/tenderfoot/templates/body/individual/individual--foaf-person.ftl index 8bc5262a..a31182c2 100644 --- a/webapp/src/main/webapp/themes/tenderfoot/templates/body/individual/individual--foaf-person.ftl +++ b/webapp/src/main/webapp/themes/tenderfoot/templates/body/individual/individual--foaf-person.ftl @@ -104,11 +104,23 @@
- - - - - + <#if editable> + <#if claimSources?size > 0> + ${i18n().claim_publications_by} + <#if claimSources?seq_contains("doi")> +
+ + +
+ + <#if claimSources?seq_contains("pmid")> +
+ + +
+ + +
diff --git a/webapp/src/main/webapp/themes/wilma/css/page-createAndLink.css b/webapp/src/main/webapp/themes/wilma/css/page-createAndLink.css new file mode 100644 index 00000000..f71cee1c --- /dev/null +++ b/webapp/src/main/webapp/themes/wilma/css/page-createAndLink.css @@ -0,0 +1,82 @@ +#createAndLink select { + height: 2.5em; + margin-top: 0px; + margin-bottom: 0px; + padding-bottom: 0px; + padding-top: 0px; +} + +#createAndLink .citation_error:before { + content: url('../../../images/createAndLink/error.png'); + transform: scale(0.5); + margin-top: 10px; + margin-right: 10px; + float: left; +} +#createAndLink .citation_error { + height: 70px; +} +#createAndLink .citation_claimed:before { + content: url('../../../images/createAndLink/tick.png'); + transform: scale(0.75); + margin-top: -17px; + margin-left: 535px; + float: left; + position: absolute; +} +#createAndLink .citation_claimed:hover:before { + opacity: 0.2; +} +#createAndLink .citation_claimed .citation { + opacity: 0.2; +} +#createAndLink .citation_claimed:hover .citation { + opacity: 1.0; +} +#createAndLink .citation_type { + font-style: italic; + padding: 5px; +} +#createAndLink .citation_title { + font-weight: bold; +} +#createAndLink .citation_journal { + font-style: italic; +} +#createAndLink .claimed { + font-weight: bold; +} +#createAndLink .linked { + font-style: italic; +} +#createAndLink .entryId { + background-color: #3e8baa; /* #E0E0E0; */ + color: #ffffff; + padding: 5px; + font-weight: bold; + display: inline-block; +} +#createAndLink .entry { + border: 2px solid #3e8baa; /* #E0E0E0; */ + padding: 5px; +} +#createAndLink label { + display: inline; +} +#createAndLink .radioWithLabel:checked + .labelForRadio { + font-weight: bold; +} +#createAndLink .description { + padding-left: 22px; +} +#createAndLink .remainder { + font-style: italic; +} +#createAndLink .claim-for { + float: right; + border: 2px solid #3e8baa; /* #E0E0E0; */ + padding: 5px; +} +#createAndLink .claim-for h3 { + text-align: center; +} diff --git a/webapp/src/main/webapp/themes/wilma/css/screen.css b/webapp/src/main/webapp/themes/wilma/css/screen.css index 7749656a..7e6c67f8 100644 --- a/webapp/src/main/webapp/themes/wilma/css/screen.css +++ b/webapp/src/main/webapp/themes/wilma/css/screen.css @@ -24,4 +24,5 @@ VIVO wilma theme: screen styles @import url("reset.css"); @import url("wilma.css"); +@import url("page-createAndLink.css"); @import url("../../../local/css/local.css"); diff --git a/webapp/src/main/webapp/themes/wilma/templates/individual--foaf-person.ftl b/webapp/src/main/webapp/themes/wilma/templates/individual--foaf-person.ftl index fb9e2424..b115ab55 100644 --- a/webapp/src/main/webapp/themes/wilma/templates/individual--foaf-person.ftl +++ b/webapp/src/main/webapp/themes/wilma/templates/individual--foaf-person.ftl @@ -62,6 +62,23 @@
<#include "individual-visualizationFoafPerson.ftl"> + <#if editable> + <#if claimSources?size > 0> +
${i18n().claim_publications_by}
+ <#if claimSources?seq_contains("doi")> +
+ + +
+ + <#if claimSources?seq_contains("pmid")> +
+ + +
+ + +
<#include "individual-adminPanel.ftl">