[VIVO-1415] Add publication claiming from PubMed and CrossRef (#129)

* Claiming interface for DOI and PMID
* Allow http://doi.org urls in the claim process
* Make button i18n compliant
* Check Editor already claimed, error for no citation, PUMCIDs fixed
* Fixed gnarly DOI containing semi-colon (removed semi-colon as separator), match more DOI formats internally, improve AUTH restrictions
* Add Wilma themes
* Add permissions to allow proxy editors
* Update DOI resolver URI for content negotiation
* CSS changes to prevent layout problems in admin pages
* Add selectable list of publication types on import

Resolves to https://jira.duraspace.org/browse/VIVO-1415
This commit is contained in:
Graham Triggs 2019-08-08 15:15:17 +01:00 committed by Andrew Woods
parent 0a08c2890d
commit f597b7ee42
31 changed files with 4197 additions and 9 deletions

View file

@ -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.config.ConfigurationProperties;
import edu.cornell.mannlib.vitro.webapp.controller.VitroRequest; import edu.cornell.mannlib.vitro.webapp.controller.VitroRequest;
import org.vivoweb.webapp.controller.freemarker.CreateAndLinkResourceController;
import javax.servlet.ServletContextEvent; import javax.servlet.ServletContextEvent;
import javax.servlet.ServletContextListener; import javax.servlet.ServletContextListener;
@ -24,6 +25,13 @@ public class VIVOIndividualResponseBuilderExtension implements IndividualRespons
public void addOptions(VitroRequest vreq, Map<String, Object> body) { public void addOptions(VitroRequest vreq, Map<String, Object> body) {
addAltMetricOptions(vreq, body); addAltMetricOptions(vreq, body);
addPlumPrintOptions(vreq, body); addPlumPrintOptions(vreq, body);
addEnabledClaimingSources(vreq, body);
}
private void addEnabledClaimingSources(VitroRequest vreq, Map<String, Object> body) {
ConfigurationProperties props = ConfigurationProperties.getBean(vreq);
body.put("claimSources", CreateAndLinkResourceController.getEnabledProviders(props));
} }
private void addAltMetricOptions(VitroRequest vreq, Map<String, Object> body) { private void addAltMetricOptions(VitroRequest vreq, Map<String, Object> body) {

View file

@ -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;
}
}
}

View file

@ -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;
}
}

View file

@ -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; }
}

View file

@ -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);
}

View file

@ -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();
}
}

View file

@ -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
}

View file

@ -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;
}
}

View file

@ -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;
}
}

View file

@ -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;
}
}

View file

@ -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<Citation.Name> 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;
}
}
}
}

View file

@ -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<Citation.Name> 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;
}
}

View file

@ -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;
}
}
}

View file

@ -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;
}
}

View file

@ -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<String[]> {
@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<String> 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;
}
}

View file

@ -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://xmlns.com/foaf/0.1/Person, foaf:Person; \
http://purl.obolibrary.org/obo/IAO_0000030, obo:IAO_0000030 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. # 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. # 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 # tpf.activeFlag = true

View file

@ -810,3 +810,55 @@ role_in_presentation_capitalized=Role in Presentation
advisee_capitalized_first_name=First Name advisee_capitalized_first_name=First Name
advisee_capitalized_lastname=Last 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<br />{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.<br />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.<br />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:<br /><br />e.g.
create_and_link_enter_dois_supported=Currently, DOIs issued by Crossref, DataCite and mEDRA are supported.<br />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

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.3 KiB

View file

@ -0,0 +1,111 @@
<#setting number_format="computer">
<form id="createAndLink" method="post">
<#if personLabel??>
<div class="claim-for">
<h3>${i18n().create_and_link_claim_for(personLabel)}</h3>
<#if personThumbUrl??>
<img src="${urls.base}${personThumbUrl}" />
</#if>
</div>
</#if>
<h2>${i18n().create_and_link_confirm_works}</h2>
${i18n().create_and_link_confirm_works_intro}<br /><br />
<h4>${i18n().create_and_link_authors}</h4>
<div class="description">${i18n().create_and_link_authors_desc}</div><br />
<h4>${i18n().create_and_link_editors}</h4>
<div class="description">${i18n().create_and_link_editors_desc}</div><br /><br />
${i18n().create_and_link_not_mine_desc}<br /><br />
<#list citations as citation>
<div class="entryId">
<#if citation.externalProvider??>
${citation.externalProvider?upper_case}: ${citation.externalId?html}
<#else>
ID: ${citation.externalId?html}
</#if>
</div>
<#if citation.type?has_content>
<select name="type${citation.externalId}">
<#list publicationTypes as publicationType>
<option value="${publicationType.uri}" <#if publicationType.uri == citation.typeUri>selected</#if>>${publicationType.label}</option>
</#list>
</select><br/>
</#if>
<div class="entry">
<#if citation.showError>
<div class="citation_error">
${i18n().create_and_link_error}
</div>
<#else>
<!-- Output Citation -->
<#if citation.alreadyClaimed>
<div class="citation_claimed">
</#if>
<input type="hidden" name="externalId" value="${citation.externalId!}" />
<div class="citation">
<#assign proposedAuthor=false />
<#if citation.title??><span class="citation_title">${citation.title?html}</span><br /></#if>
<#assign formatted_citation>
<#if citation.journal??><span class="citation_journal">${citation.journal?html}</span></#if>
<#if citation.publicationYear??><span class="citation_year">${citation.publicationYear!?html};</span></#if>
<#if citation.volume??><span class="citation_volume">${citation.volume!?html}</#if>
<#if citation.issue??><span class="citation_issue">(${citation.issue!?html})</#if>
<#if citation.pagination??><span class="citation_pages">:${citation.pagination!?html}</#if>
</#assign>
<#if formatted_citation??>
${formatted_citation}<br />
</#if>
<#if citation.authors??>
<#list citation.authors as author>
<#if author??>
<span class="citation_author">
<#if citation.alreadyClaimed>
<span>${author.name!?html}</span>
<#else>
<#if author.name??>
<#if !author.linked>
<input type="radio" id="author${citation.externalId}-${author?counter}" name="contributor${citation.externalId}" value="author${author?counter}" <#if author.proposed>checked</#if> class="radioWithLabel" />
<label for="author${citation.externalId}-${author?counter}" class="labelForRadio">${author.name!?html}</label>
<#if author.proposed><#assign proposedAuthor=true /></#if>
<#else>
<span class="linked">${author.name!?html}</span>
</#if>
</#if>
</#if>
</span>
</#if>
<#sep>; </#sep>
</#list><br />
</#if>
</div>
<#if citation.alreadyClaimed>
<span class="claimed">${i18n().create_and_link_already_claimed}</span>
<#else>
<input type="radio" id="author${citation.externalId}" name="contributor${citation.externalId}" value="author" <#if !proposedAuthor>checked</#if> class="radioWithLabel" /><label for="author${citation.externalId}" class="labelForRadio"> ${i18n().create_and_link_unlisted_author}</label><br />
<input type="radio" id="editor${citation.externalId}" name="contributor${citation.externalId}" value="editor" class="radioWithLabel" /><label for="editor${citation.externalId}" class="labelForRadio"> ${i18n().create_and_link_editor}</label><br />
<input type="radio" id="notmine${citation.externalId}" name="contributor${citation.externalId}" value="notmine" class="radioWithLabel" /><label for="notmine${citation.externalId}" class="labelForRadio"> ${i18n().create_and_link_not_mine}</label><br />
</#if>
<input type="hidden" name="externalResource${citation.externalId}" value="${citation.externalResource!?html}" />
<input type="hidden" name="externalProvider${citation.externalId}" value="${citation.externalProvider!?html}" />
<input type="hidden" name="vivoUri${citation.externalId}" value="${citation.vivoUri!?html}" />
<input type="hidden" name="profileUri" value="${profileUri!}" />
<#if citation.alreadyClaimed>
</div>
</#if>
</#if>
<div style="clear: both;"></div>
</div>
<br/>
<!-- End Citation -->
</#list>
<#if remainderIds??>
<input type="hidden" name="remainderIds" value="${remainderIds}" />
</#if>
<div class="buttons">
<input type="hidden" name="action" value="confirmID" />
<input type="submit" value="${i18n().create_and_link_submit_confirm}" class="submit" />
<#if remainderCount??>
<span class="remainder">${i18n().create_and_link_remaining(remainderCount)}</span>
</#if>
</div>
</form>

View file

@ -0,0 +1,35 @@
<form id="createAndLink" method="post">
<#if personLabel??>
<div class="claim-for">
<h3>${i18n().create_and_link_claim_for(personLabel)}</h3>
<#if personThumbUrl??>
<img src="${urls.base}${personThumbUrl}" />
</#if>
</div>
</#if>
<#if showConfirmation??>
<h2>${i18n().create_and_link_thank_you}</h2>
${i18n().create_and_link_finished}<br /><br />
<#if profileUri??>
<a href="${profileUrl(profileUri)}">${i18n().create_and_link_go_profile}</a><br /><br />
</#if>
</#if>
<h2>${i18n().create_and_link_enter(label)}</h2>
<#switch provider>
<#case "doi">
${i18n().create_and_link_enter_dois_intro}<br />
<i>ID</i>: 10.1038/nature01234<br />
<i>URL</i>: https://doi.org/10.1038/nature01234<br />
<br />
${i18n().create_and_link_enter_dois_supported}<br /><br />
<#break>
<#case "pmid">
${i18n().create_and_link_enter_pmid_intro}<br /><br />
${i18n().create_and_link_enter_pmid_supported}<br /><br />
<#break>
</#switch>
<textarea name="externalIds" rows="15" cols="50"></textarea><br />
<input type="submit" value="${i18n().create_and_link_submit_ids}" class="submit" /><br />
<input type="hidden" name="action" value="findID" />
<input type="hidden" name="profileUri" value="${profileUri!}" />
</form>

View file

@ -0,0 +1 @@
<h2>${i18n().create_and_link_unauthorized_for_profile}</h2>

View file

@ -0,0 +1,2 @@
<h2>${i18n().create_and_link_unknown_profile}</h2>

View file

@ -0,0 +1,2 @@
<h2>${i18n().create_and_link_unknown_resource}</h2>

View file

@ -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;
}

View file

@ -30,5 +30,6 @@ VIVO tenderfoot theme: screen styles
@import url("page-individual.css"); @import url("page-individual.css");
@import url("page-login.css"); @import url("page-login.css");
@import url("page-menu.css"); @import url("page-menu.css");
@import url("page-createAndLink.css");
@import url("https://fonts.googleapis.com/css?family=Noto+Sans"); @import url("https://fonts.googleapis.com/css?family=Noto+Sans");
@import url("../../../local/css/local.css"); @import url("../../../local/css/local.css");

View file

@ -104,11 +104,23 @@
<section itemscope itemtype="http://schema.org/Person" id="individual-intro" class="vcard person" role="region"> <section itemscope itemtype="http://schema.org/Person" id="individual-intro" class="vcard person" role="region">
<section id="individual-info" ${infoClass!} role="region"> <section id="individual-info" ${infoClass!} role="region">
<!-- Overview --> <#if editable>
<!-- #include "individual-overview.ftl" --> <#if claimSources?size &gt; 0>
${i18n().claim_publications_by}
<!-- Geographic Focus --> <#if claimSources?seq_contains("doi")>
<!-- #include "individual-geographicFocus.ftl" --> <form action="${urls.base}/createAndLink/doi" method="get" style="display: inline-block;">
<input type="hidden" name="profileUri" value="${individual.uri}" />
<input type="submit" class="submit" value="${i18n().claim_publications_by_doi}" />
</form>
</#if>
<#if claimSources?seq_contains("pmid")>
<form action="${urls.base}/createAndLink/pmid" method="get" style="display: inline-block;">
<input type="hidden" name="profileUri" value="${individual.uri}" />
<input type="submit" class="submit" value="${i18n().claim_publications_by_pmid}" />
</form>
</#if>
</#if>
</#if>
</section> </section>
</section> </section>

View file

@ -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;
}

View file

@ -24,4 +24,5 @@ VIVO wilma theme: screen styles
@import url("reset.css"); @import url("reset.css");
@import url("wilma.css"); @import url("wilma.css");
@import url("page-createAndLink.css");
@import url("../../../local/css/local.css"); @import url("../../../local/css/local.css");

View file

@ -62,6 +62,23 @@
<section id="individual-info" ${infoClass!} role="region"> <section id="individual-info" ${infoClass!} role="region">
<section id="right-hand-column" role="region"> <section id="right-hand-column" role="region">
<#include "individual-visualizationFoafPerson.ftl"> <#include "individual-visualizationFoafPerson.ftl">
<#if editable>
<#if claimSources?size &gt; 0>
<br />${i18n().claim_publications_by}<br />
<#if claimSources?seq_contains("doi")>
<form action="${urls.base}/createAndLink/doi" method="get" style="float: left;">
<input type="hidden" name="profileUri" value="${individual.uri}" />
<input type="submit" class="submit" value="${i18n().claim_publications_by_doi}" />
</form>
</#if>
<#if claimSources?seq_contains("pmid")>
<form action="${urls.base}/createAndLink/pmid" method="get" style="float: right;">
<input type="hidden" name="profileUri" value="${individual.uri}" />
<input type="submit" class="submit" value="${i18n().claim_publications_by_pmid}" />
</form>
</#if>
</#if>
</#if>
</section> </section>
<#include "individual-adminPanel.ftl"> <#include "individual-adminPanel.ftl">