diff --git a/config/example.runtime.properties b/config/example.runtime.properties index 3034e0dc..81ae7e0f 100644 --- a/config/example.runtime.properties +++ b/config/example.runtime.properties @@ -198,6 +198,31 @@ VitroConnection.DataSource.validationQuery = SELECT 1 # languages.selectableLocales = en_US, es_GO +# ----------------------------------------------------------------------------- +# ORCID INTEGRATION +# ----------------------------------------------------------------------------- + +# orcid.clientId = 0000-0000-0000-000X +# orcid.clientPassword = 00000000-0000-0000-0000-000000000000 +# orcid.webappBaseUrl = http://localhost:8080/vivo +# orcid.messageVersion = 1.0.23 +# orcid.externalIdCommonName = VIVO Cornell Identifier + +# ---- Setup for the sandbox ---- + +# orcid.publicApiBaseUrl = http://pub.sandbox-1.orcid.org/v1.1 +# orcid.authorizedApiBaseUrl = http://api.sandbox-1.orcid.org/v1.1 +# orcid.oauthAuthorizeUrl = http://sandbox-1.orcid.org/oauth/authorize +# orcid.oauthTokenUrl = http://api.sandbox-1.orcid.org/oauth/token + +# ---- or for the mockorcid webapp ---- + +# orcid.publicApiBaseUrl = http://localhost:8080/mockorcid/mock/ +# orcid.authorizedApiBaseUrl = http://localhost:8080/mockorcid/mock/ +# orcid.oauthAuthorizeUrl = http://localhost:8080/mockorcid/mock/oauth/authorize +# orcid.oauthTokenUrl = http://localhost:8080/mockorcid/mock/oauth/token + + # ----------------------------------------------------------------------------- # OTHER OPTIONS # ----------------------------------------------------------------------------- diff --git a/doc/3rd-party-licenses.txt b/doc/3rd-party-licenses.txt index 6bf231e6..ed817b72 100644 --- a/doc/3rd-party-licenses.txt +++ b/doc/3rd-party-licenses.txt @@ -191,3 +191,7 @@ xsdlib None ---- Trippi + +ORCID API client +---------------- +Written as part of the ORCID A&I grant. Source is available at https://github.com/j2blake/orcid-api-client \ No newline at end of file diff --git a/lib/orcid-api-client-0.1.jar b/lib/orcid-api-client-0.1.jar new file mode 100644 index 00000000..bbf44d93 Binary files /dev/null and b/lib/orcid-api-client-0.1.jar differ diff --git a/productMods/WEB-INF/resources/startup_listeners.txt b/productMods/WEB-INF/resources/startup_listeners.txt index 4040cc02..ded2dccd 100644 --- a/productMods/WEB-INF/resources/startup_listeners.txt +++ b/productMods/WEB-INF/resources/startup_listeners.txt @@ -85,5 +85,7 @@ org.apache.commons.fileupload.servlet.FileCleanerCleanup # and the PermissionRegistry must already be set up. edu.cornell.mannlib.vitro.webapp.dao.jena.VClassGroupCache$Setup +edu.cornell.mannlib.vivo.orcid.OrcidContextSetup + # This should be near the end, because it will issue a warning if the connection to Solr times out. edu.cornell.mannlib.vitro.webapp.servlet.setup.SolrSmokeTest diff --git a/productMods/WEB-INF/web.xml b/productMods/WEB-INF/web.xml index fcc2e63f..d5d2dbd2 100644 --- a/productMods/WEB-INF/web.xml +++ b/productMods/WEB-INF/web.xml @@ -1400,7 +1400,15 @@ /searchService/* - + + OrcidIntegrationController + edu.cornell.mannlib.vivo.orcid.controller.OrcidIntegrationController + + + OrcidIntegrationController + /orcid/* + + diff --git a/productMods/templates/freemarker/body/orcid/orcidConfirm.ftl b/productMods/templates/freemarker/body/orcid/orcidConfirm.ftl new file mode 100644 index 00000000..f962d932 --- /dev/null +++ b/productMods/templates/freemarker/body/orcid/orcidConfirm.ftl @@ -0,0 +1,148 @@ +<#-- $This file is distributed under the terms of the license in /doc/license.txt$ --> + +<#-- +The body map contains the orcidInfo structure, which is set up like this: + +orcidInfo + progress - a string set to one of these values: START, DENIED_AUTHENTICATE, + FAILED_AUTHENTICATE, GOT_PROFILE, ID_ALREADY_PRESENT, DENIED_ID, + FAILED_ID, ADDED_ID + individualUri - the URI of the person + profilePage - the URL of the individual's profile page + orcid - the confirmed ORCID (just xxxx-xxxx-xxxx-xxxx), + or the empty string. + orcidUri - the confirmed ORCID (full URI), or the empty string. + externalIds - empty if we haven't read their profile. Otherwise, a sequence + of maps, one for each external ID in their profile. These + might include SCOPUS ID, etc. Each map looks like this: + commonName - e.g., "VIVO Cornell" + reference - e.g., their VIVO localname + uri - e.g., their VIVO URI + hasVivoId - true, if we have read the profile and they already have + their VIVO URI as an external ID. False otherwise. + existingOrcids - A sequence of the ORCIDs (full URI) that we already associate + with this individual. + progressUrl - The URL to go to, that will continue this process. If the + process is complete or has failed, this is empty. + +--> + + + +<#assign orcidTextOne = "add an" /> +<#assign orcidTextTwo = "Adding" /> +<#if (orcidInfo.existingOrcids?size > 0) > + <#assign orcidTextOne = "confirm your" /> + <#assign orcidTextTwo = "Confirming" /> + +<#assign step2dimmed = (["START", "FAILED_AUTHENTICATE", "DENIED_AUTHENTICATE"]?seq_contains(orcidInfo.progress))?string("dimmed", "") /> +<#assign continueAppears = (["START", "GOT_PROFILE"]?seq_contains(orcidInfo.progress))/> + +
+ +
+

Do you want to ${orcidTextOne} ORCID Identification?

+ +
+ <#if "START" == orcidInfo.progress> +

Step 1: ${orcidTextTwo} your ORCID ID

+
    +
  • VIVO redirects you to ORCID's web site.
  • +
  • You log in to your ORCID account. +
    • If you don't have an account, you can create one.
    +
  • +
  • You tell ORCID that VIVO may read your ORCID Record. (one-time permission)
  • +
  • VIVO reads your ORCID Record.
  • +
  • VIVO notes that your ORCID ID is confirmed.
  • +
+ <#elseif "DENIED_AUTHENTICATE" == orcidInfo.progress> +

Step 1: ${orcidTextTwo} your ORCID ID

+

You denied VIVO's request to read your ORCID profile.

+

Confirmation can't continue.

+ <#elseif "FAILED_AUTHENTICATE" == orcidInfo.progress> +

Step 1: ${orcidTextTwo} your ORCID ID

+

VIVO failed to read your ORCID profile.

+

Confirmation can't continue.

+ <#else> +

Step 1: ${orcidTextTwo} your ORCID ID (step completed)

+

Your ORCID ID is confirmed as ${orcidInfo.orcid}

+

View your ORCID profile page.

+ +
+ +
+ <#if "ID_ALREADY_PRESENT" == orcidInfo.progress> +

Step 2 (recommended): Linking your ORCID Record to VIVO (step completed)

+

Your ORCID profile already includes a link to VIVO.

+ <#elseif "DENIED_ID" == orcidInfo.progress> +

Step 2 (recommended): Linking your ORCID Record to VIVO

+

You denied VIVO's request to add an External ID to your ORCID profile.

+

Linking can't continue.

+ <#elseif "FAILED_ID" == orcidInfo.progress> +

Step 2 (recommended): Linking your ORCID Record to VIVO

+

VIVO failed to add an External ID to your ORCID profile.

+

Linking can't continue.

+ <#elseif "ADDED_ID" == orcidInfo.progress> +

Step 2 (recommended): Linking your ORCID Record to VIVO (step completed)

+

Your ORCID profile is linked to VIVO

+

View your ORCID profile page.

+ <#else> +

Step 2 (recommended): Linking your ORCID Record to VIVO

+
    +
  • VIVO redirects you to ORCID's web site
  • +
  • You tell ORCID that VIVO may add an "external ID" to your ORCID Record. (one-time permission)
  • +
  • VIVO adds the external ID.
  • +
+ +
+ + +
+ +
\ No newline at end of file diff --git a/productMods/templates/freemarker/body/partials/individual/individual-orcidInterface.ftl b/productMods/templates/freemarker/body/partials/individual/individual-orcidInterface.ftl new file mode 100644 index 00000000..a096b89a --- /dev/null +++ b/productMods/templates/freemarker/body/partials/individual/individual-orcidInterface.ftl @@ -0,0 +1,34 @@ +<#-- $This file is distributed under the terms of the license in /doc/license.txt$ --> + +<#-- + If authorized to confirm ORCID IDs, add the function that will replace the add link. + The OrcidIdDataGetter is attached to this template; it sets the orcidInfo structure, + which looks like this: + + orcidInfo = map { + authorizedToConfirm: boolean + orcidUrl: link to the orcid controller + orcids: map of String to boolean [ + orcid: String (full URI) + confirmed: boolean + ] + } +--> +<#assign confirmThis = "" /> +<#if orcidInfo??> + + <#list orcidInfo.orcids?keys as key> + <#if "no" == orcidInfo.orcids[key]?string("yes","no") > + <#assign confirmThis = "Confirm the ID" /> + + + + <#if orcidInfo.authorizedToConfirm> + + + + \ No newline at end of file diff --git a/productMods/templates/freemarker/body/partials/individual/propStatement-orcidId.ftl b/productMods/templates/freemarker/body/partials/individual/propStatement-orcidId.ftl index 2a433a3e..1b8c265f 100644 --- a/productMods/templates/freemarker/body/partials/individual/propStatement-orcidId.ftl +++ b/productMods/templates/freemarker/body/partials/individual/propStatement-orcidId.ftl @@ -9,6 +9,13 @@ <#macro showStatement statement> ${statement.value!"ORCID iD not found"} + <#if orcidInfo??> + <#if (orcidInfo.orcids[statement.value])!false> + (confirmed) + <#else> + (pending confirmation) + + diff --git a/rdf/abox/filegraph/validation.n3 b/rdf/abox/filegraph/validation.n3 new file mode 100644 index 00000000..4e4d08c9 --- /dev/null +++ b/rdf/abox/filegraph/validation.n3 @@ -0,0 +1,2 @@ + + . diff --git a/rdf/display/everytime/orcidInterfaceDataGetters.n3 b/rdf/display/everytime/orcidInterfaceDataGetters.n3 new file mode 100644 index 00000000..225ff88a --- /dev/null +++ b/rdf/display/everytime/orcidInterfaceDataGetters.n3 @@ -0,0 +1,14 @@ +# $This file is distributed under the terms of the license in /doc/license.txt$ + +@prefix foaf: . +@prefix display: . + +# +# datagetter to fetch ORCID info for the person. +# + + display:hasDataGetter display:orcidIdDataGetter . + +display:orcidIdDataGetter + a . + diff --git a/rdf/tbox/filegraph/orcid-interface.n3 b/rdf/tbox/filegraph/orcid-interface.n3 new file mode 100644 index 00000000..d4fa00ba --- /dev/null +++ b/rdf/tbox/filegraph/orcid-interface.n3 @@ -0,0 +1,10 @@ +@prefix rdfs: . +@prefix owl: . +@prefix core: . +@prefix foaf: . + +core:confirmedOrcidId + a owl:ObjectProperty ; + rdfs:label "Orcid ID confirmation"@en-US ; + rdfs:comment "Indicates that the Orcid ID has been confirmed by this Person" ; + rdfs:range foaf:Person . diff --git a/src/edu/cornell/mannlib/vivo/orcid/OrcidContextSetup.java b/src/edu/cornell/mannlib/vivo/orcid/OrcidContextSetup.java new file mode 100644 index 00000000..4eca45f1 --- /dev/null +++ b/src/edu/cornell/mannlib/vivo/orcid/OrcidContextSetup.java @@ -0,0 +1,107 @@ +/* $This file is distributed under the terms of the license in /doc/license.txt$ */ + +package edu.cornell.mannlib.vivo.orcid; + +import static edu.cornell.mannlib.orcidclient.context.OrcidClientContext.Setting.AUTHORIZED_API_BASE_URL; +import static edu.cornell.mannlib.orcidclient.context.OrcidClientContext.Setting.CALLBACK_PATH; +import static edu.cornell.mannlib.orcidclient.context.OrcidClientContext.Setting.CLIENT_ID; +import static edu.cornell.mannlib.orcidclient.context.OrcidClientContext.Setting.CLIENT_SECRET; +import static edu.cornell.mannlib.orcidclient.context.OrcidClientContext.Setting.MESSAGE_VERSION; +import static edu.cornell.mannlib.orcidclient.context.OrcidClientContext.Setting.OAUTH_AUTHORIZE_URL; +import static edu.cornell.mannlib.orcidclient.context.OrcidClientContext.Setting.OAUTH_TOKEN_URL; +import static edu.cornell.mannlib.orcidclient.context.OrcidClientContext.Setting.PUBLIC_API_BASE_URL; +import static edu.cornell.mannlib.orcidclient.context.OrcidClientContext.Setting.WEBAPP_BASE_URL; +import static edu.cornell.mannlib.vivo.orcid.controller.OrcidIntegrationController.DEFAULT_EXTERNAL_ID_COMMON_NAME; +import static edu.cornell.mannlib.vivo.orcid.controller.OrcidIntegrationController.PROPERTY_EXTERNAL_ID_COMMON_NAME; + +import java.util.EnumMap; +import java.util.Map; + +import javax.servlet.ServletContext; +import javax.servlet.ServletContextEvent; +import javax.servlet.ServletContextListener; + +import org.apache.commons.lang.StringUtils; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; + +import edu.cornell.mannlib.orcidclient.OrcidClientException; +import edu.cornell.mannlib.orcidclient.context.OrcidClientContext; +import edu.cornell.mannlib.orcidclient.context.OrcidClientContext.Setting; +import edu.cornell.mannlib.vitro.webapp.config.ConfigurationProperties; +import edu.cornell.mannlib.vitro.webapp.startup.StartupStatus; + +/** + * Setup for the ORCID interface. + * + * Note that the property for CLIENT_SECRET is "orcid.clientPassword". Since it + * ends in "password", it will not be displayed on the ShowConfiguration page. + * + * The CALLBACK_PATH is hardcoded. It is relative to the WEBAPP_BASE_URL, so it + * won't change. + */ +public class OrcidContextSetup implements ServletContextListener { + private static final Log log = LogFactory.getLog(OrcidContextSetup.class); + + @Override + public void contextInitialized(ServletContextEvent sce) { + ServletContext ctx = sce.getServletContext(); + ConfigurationProperties props = ConfigurationProperties.getBean(ctx); + StartupStatus ss = StartupStatus.getBean(ctx); + + if (props.getProperty("orcid.clientId", "").isEmpty()) { + ss.info(this, "ORCID Integration is not configured."); + return; + } + + initializeOrcidClientContext(props, ss); + + checkForCommonNameProperty(props, ss); + } + + private void initializeOrcidClientContext(ConfigurationProperties props, + StartupStatus ss) { + try { + Map settings = new EnumMap<>(Setting.class); + settings.put(CLIENT_ID, props.getProperty("orcid.clientId")); + settings.put(CLIENT_SECRET, + props.getProperty("orcid.clientPassword")); + settings.put(PUBLIC_API_BASE_URL, + props.getProperty("orcid.publicApiBaseUrl")); + settings.put(AUTHORIZED_API_BASE_URL, + props.getProperty("orcid.authorizedApiBaseUrl")); + settings.put(OAUTH_AUTHORIZE_URL, + props.getProperty("orcid.oauthAuthorizeUrl")); + settings.put(OAUTH_TOKEN_URL, + props.getProperty("orcid.oauthTokenUrl")); + settings.put(MESSAGE_VERSION, + props.getProperty("orcid.messageVersion")); + settings.put(WEBAPP_BASE_URL, + props.getProperty("orcid.webappBaseUrl")); + settings.put(CALLBACK_PATH, "orcid/callback"); + + OrcidClientContext.initialize(settings); + ss.info(this, "Context is: " + OrcidClientContext.getInstance()); + + } catch (OrcidClientException e) { + ss.warning(this, "Failed to initialize OrcidClientContent", e); + } + } + + private void checkForCommonNameProperty(ConfigurationProperties props, + StartupStatus ss) { + if (StringUtils.isBlank(props + .getProperty(PROPERTY_EXTERNAL_ID_COMMON_NAME))) { + ss.warning(this, "'" + PROPERTY_EXTERNAL_ID_COMMON_NAME + + "' is not set. " + "Using default value of '" + + DEFAULT_EXTERNAL_ID_COMMON_NAME + "'"); + + } + } + + @Override + public void contextDestroyed(ServletContextEvent sce) { + // Nothing to tear down. + } + +} diff --git a/src/edu/cornell/mannlib/vivo/orcid/OrcidIdDataGetter.java b/src/edu/cornell/mannlib/vivo/orcid/OrcidIdDataGetter.java new file mode 100644 index 00000000..5305d7f6 --- /dev/null +++ b/src/edu/cornell/mannlib/vivo/orcid/OrcidIdDataGetter.java @@ -0,0 +1,217 @@ +/* $This file is distributed under the terms of the license in /doc/license.txt$ */ + +package edu.cornell.mannlib.vivo.orcid; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; + +import com.hp.hpl.jena.query.QuerySolution; +import com.hp.hpl.jena.query.ResultSet; +import com.hp.hpl.jena.rdf.model.RDFNode; +import com.hp.hpl.jena.rdf.model.Resource; + +import edu.cornell.mannlib.vitro.webapp.auth.identifier.IdentifierBundle; +import edu.cornell.mannlib.vitro.webapp.auth.identifier.RequestIdentifiers; +import edu.cornell.mannlib.vitro.webapp.auth.identifier.common.HasAssociatedIndividual; +import edu.cornell.mannlib.vitro.webapp.auth.identifier.common.HasProfile; +import edu.cornell.mannlib.vitro.webapp.auth.identifier.common.HasProxyEditingRights; +import edu.cornell.mannlib.vitro.webapp.auth.identifier.common.IsRootUser; +import edu.cornell.mannlib.vitro.webapp.controller.VitroRequest; +import edu.cornell.mannlib.vitro.webapp.controller.freemarker.UrlBuilder; +import edu.cornell.mannlib.vitro.webapp.utils.SparqlQueryRunner; +import edu.cornell.mannlib.vitro.webapp.utils.SparqlQueryRunner.QueryParser; +import edu.cornell.mannlib.vitro.webapp.utils.dataGetter.DataGetter; +import edu.cornell.mannlib.vivo.orcid.controller.OrcidIntegrationController; + +/** + * This data getter should be assigned to the template that renders the list + * view for ORCID IDs. + * + * Find out whether the user is authorized to confirm the ORCID IDs on this + * page. Find the list of ORCID IDs, and whether each has already been + * confirmed. + * + * The information is stored in the values map like this: + * + *
+ *    orcidInfo = map {
+ *        authorizedToConfirm: boolean
+ *        orcids: map of String to boolean [
+ *            orcid: String
+ *            confirm: boolean
+ *        ]
+ *    }
+ * 
+ */ +public class OrcidIdDataGetter implements DataGetter { + private static final Log log = LogFactory.getLog(OrcidIdDataGetter.class); + + private static final Map EMPTY_RESULT = Collections + .emptyMap(); + public static final String ORCID_ID = "http://vivoweb.org/ontology/core#orcidId"; + public static final String ORCID_IS_CONFIRMED = "http://vivoweb.org/ontology/core#confirmedOrcidId"; + private static final String QUERY_TEMPLATE = "SELECT ?orcid ?confirmed \n" + + "WHERE { \n" // + + " <%s> <%s> ?orcid . \n" // + + " OPTIONAL { \n" // + + " ?orcid <%s> ?confirmed . \n" // + + " } \n" // + + "}\n"; + + private final VitroRequest vreq; + + public OrcidIdDataGetter(VitroRequest vreq) { + this.vreq = vreq; + } + + @Override + public Map getData(Map valueMap) { + try { + String individualUri = findIndividualUri(valueMap); + if (individualUri == null) { + return EMPTY_RESULT; + } + + boolean isAuthorizedToConfirm = figureIsAuthorizedtoConfirm(individualUri); + List orcids = runSparqlQuery(individualUri); + return buildMap(isAuthorizedToConfirm, orcids, individualUri); + } catch (Exception e) { + log.warn("Failed to get orcID information", e); + return EMPTY_RESULT; + } + } + + private String findIndividualUri(Map valueMap) { + try { + String uri = (String) valueMap.get("individualURI"); + + if (uri == null) { + log.warn("valueMap has no individualURI. Keys are: " + + valueMap.keySet()); + return null; + } else { + return uri; + } + } catch (Exception e) { + log.debug("has a problem finding the individualURI", e); + return null; + } + + } + + /** + * You are authorized to confirm an orcId only if you are a self-editor or + * root. + */ + private boolean figureIsAuthorizedtoConfirm(String individualUri) { + IdentifierBundle ids = RequestIdentifiers.getIdBundleForRequest(vreq); + boolean isSelfEditor = HasProfile.getProfileUris(ids).contains( + individualUri); + boolean isProxyEditor = HasProxyEditingRights.getProxiedPageUris(ids) + .contains(individualUri); + boolean isRoot = IsRootUser.isRootUser(ids); + return isRoot || isProxyEditor || isSelfEditor; + } + + private List runSparqlQuery(String individualUri) { + String queryStr = String.format(QUERY_TEMPLATE, individualUri, + ORCID_ID, ORCID_IS_CONFIRMED); + SparqlQueryRunner runner = new SparqlQueryRunner(vreq.getJenaOntModel()); + return runner.executeSelect(new OrcidResultParser(), queryStr); + } + + private Map buildMap(boolean isAuthorizedToConfirm, + List orcids, String individualUri) { + Map confirmationMap = new HashMap<>(); + for (OrcidInfo oInfo : orcids) { + confirmationMap.put(oInfo.getOrcid(), oInfo.isConfirmed()); + } + + Map orcidInfoMap = new HashMap<>(); + orcidInfoMap.put("authorizedToConfirm", isAuthorizedToConfirm); + orcidInfoMap.put("orcidUrl", UrlBuilder.getUrl( + OrcidIntegrationController.PATH_DEFAULT, "individualUri", + individualUri)); + orcidInfoMap.put("orcids", confirmationMap); + + Map map = new HashMap<>(); + map.put("orcidInfo", orcidInfoMap); + + log.debug("Returning these values:" + map); + return map; + } + + // ---------------------------------------------------------------------- + // Helper classes + // ---------------------------------------------------------------------- + + /** + * Parse the results of the SPARQL query. + */ + private static class OrcidResultParser extends QueryParser> { + @Override + protected List defaultValue() { + return Collections.emptyList(); + } + + @Override + protected List parseResults(String queryStr, + ResultSet results) { + List orcids = new ArrayList<>(); + + while (results.hasNext()) { + try { + QuerySolution solution = results.next(); + Resource orcid = solution.getResource("orcid"); + RDFNode cNode = solution.get("confirmed"); + log.debug("Result is orcid=" + orcid + ", confirmed=" + + cNode); + + if (orcid != null && orcid.isURIResource()) { + boolean confirmed = (cNode != null); + orcids.add(new OrcidInfo(orcid.getURI(), confirmed)); + } + } catch (Exception e) { + log.warn("Failed to parse the query result: " + queryStr, e); + } + } + + return orcids; + } + } + + /** + * A bean to hold info for each ORCID. + */ + static class OrcidInfo { + private final String orcid; + private final boolean confirmed; + + public OrcidInfo(String orcid, boolean confirmed) { + this.orcid = orcid; + this.confirmed = confirmed; + } + + public String getOrcid() { + return orcid; + } + + public boolean isConfirmed() { + return confirmed; + } + + @Override + public String toString() { + return "OrcidInfo[orcid=" + orcid + ", confirmed=" + confirmed + + "]"; + } + + } + +} diff --git a/src/edu/cornell/mannlib/vivo/orcid/controller/OrcidAbstractHandler.java b/src/edu/cornell/mannlib/vivo/orcid/controller/OrcidAbstractHandler.java new file mode 100644 index 00000000..6bd1e49f --- /dev/null +++ b/src/edu/cornell/mannlib/vivo/orcid/controller/OrcidAbstractHandler.java @@ -0,0 +1,124 @@ +/* $This file is distributed under the terms of the license in /doc/license.txt$ */ + +package edu.cornell.mannlib.vivo.orcid.controller; + +import static edu.cornell.mannlib.vitro.webapp.dao.VitroVocabulary.OWL_THING; +import static edu.cornell.mannlib.vitro.webapp.dao.VitroVocabulary.RDF_TYPE; +import static edu.cornell.mannlib.vivo.orcid.OrcidIdDataGetter.ORCID_ID; +import static edu.cornell.mannlib.vivo.orcid.OrcidIdDataGetter.ORCID_IS_CONFIRMED; +import static edu.cornell.mannlib.vivo.orcid.controller.OrcidIntegrationController.TEMPLATE_CONFIRM; +import static javax.servlet.http.HttpServletResponse.SC_INTERNAL_SERVER_ERROR; + +import java.util.HashMap; +import java.util.Map; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; + +import edu.cornell.mannlib.orcidclient.auth.AuthorizationManager; +import edu.cornell.mannlib.orcidclient.context.OrcidClientContext; +import edu.cornell.mannlib.orcidclient.orcidmessage.OrcidMessage; +import edu.cornell.mannlib.vedit.beans.LoginStatusBean; +import edu.cornell.mannlib.vitro.webapp.beans.Individual; +import edu.cornell.mannlib.vitro.webapp.beans.ObjectPropertyStatement; +import edu.cornell.mannlib.vitro.webapp.beans.ObjectPropertyStatementImpl; +import edu.cornell.mannlib.vitro.webapp.beans.UserAccount; +import edu.cornell.mannlib.vitro.webapp.controller.VitroRequest; +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.ObjectPropertyStatementDao; +import edu.cornell.mannlib.vivo.orcid.controller.OrcidConfirmationState.Progress; + +/** + * Some utility methods for the handlers. + */ +public abstract class OrcidAbstractHandler { + private static final Log log = LogFactory + .getLog(OrcidAbstractHandler.class); + + protected final VitroRequest vreq; + protected final OrcidClientContext occ; + protected final AuthorizationManager auth; + protected final OrcidConfirmationState state; + protected final UserAccount currentUser; + + protected OrcidAbstractHandler(VitroRequest vreq) { + this.vreq = vreq; + this.occ = OrcidClientContext.getInstance(); + this.auth = this.occ.getAuthorizationManager(vreq); + this.state = OrcidConfirmationState.fetch(vreq); + this.currentUser = LoginStatusBean.getCurrentUser(vreq); + } + + protected Individual findIndividual() { + String uri = state.getIndividualUri(); + try { + IndividualDao iDao = vreq.getWebappDaoFactory().getIndividualDao(); + Individual individual = iDao.getIndividualByURI(uri); + if (individual == null) { + throw new IllegalStateException("Individual URI not valid: '" + + uri + "'"); + } + return individual; + } catch (Exception e) { + throw new IllegalStateException("Individual URI not valid: '" + uri + + "'"); + } + } + + protected void recordConfirmation() { + String individualUri = state.getIndividualUri(); + String orcidUri = state.getOrcidUri(); + log.debug("Recording confirmation of ORCID '" + orcidUri + "' on '" + + individualUri + "'"); + ObjectPropertyStatement ops1 = new ObjectPropertyStatementImpl( + individualUri, ORCID_ID, orcidUri); + ObjectPropertyStatement ops2 = new ObjectPropertyStatementImpl( + orcidUri, RDF_TYPE, OWL_THING); + ObjectPropertyStatement ops3 = new ObjectPropertyStatementImpl( + orcidUri, ORCID_IS_CONFIRMED, individualUri); + + ObjectPropertyStatementDao opsd = vreq.getWebappDaoFactory() + .getObjectPropertyStatementDao(); + opsd.insertNewObjectPropertyStatement(ops1); + opsd.insertNewObjectPropertyStatement(ops2); + opsd.insertNewObjectPropertyStatement(ops3); + } + + protected String cornellNetId() { + if (currentUser == null) { + return null; + } + String externalId = currentUser.getExternalAuthId(); + if (externalId == null) { + return null; + } + if (externalId.trim().isEmpty()) { + return null; + } + return externalId; + } + + protected ResponseValues show500InternalServerError(String message) { + log.error("Problem with ORCID request: " + message); + Map map = new HashMap<>(); + map.put("title", "500 Internal Server Error"); + map.put("errorMessage", message); + return new TemplateResponseValues("error-titled.ftl", map, + SC_INTERNAL_SERVER_ERROR); + } + + protected ResponseValues showConfirmationPage(Progress p, + OrcidMessage... messages) { + state.progress(p, messages); + return showConfirmationPage(); + } + + protected ResponseValues showConfirmationPage() { + Map map = new HashMap<>(); + map.put("orcidInfo", state.toMap()); + return new TemplateResponseValues(TEMPLATE_CONFIRM, map); + } + +} diff --git a/src/edu/cornell/mannlib/vivo/orcid/controller/OrcidAddExternalIdHandler.java b/src/edu/cornell/mannlib/vivo/orcid/controller/OrcidAddExternalIdHandler.java new file mode 100644 index 00000000..8e8f86e9 --- /dev/null +++ b/src/edu/cornell/mannlib/vivo/orcid/controller/OrcidAddExternalIdHandler.java @@ -0,0 +1,60 @@ +/* $This file is distributed under the terms of the license in /doc/license.txt$ */ + +package edu.cornell.mannlib.vivo.orcid.controller; + +import static edu.cornell.mannlib.orcidclient.actions.ApiAction.ADD_EXTERNAL_ID; +import static edu.cornell.mannlib.orcidclient.orcidmessage.Visibility.PUBLIC; +import static edu.cornell.mannlib.vivo.orcid.controller.OrcidConfirmationState.Progress.ADDED_ID; +import static edu.cornell.mannlib.vivo.orcid.controller.OrcidConfirmationState.Progress.DENIED_ID; +import static edu.cornell.mannlib.vivo.orcid.controller.OrcidConfirmationState.Progress.FAILED_ID; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; + +import edu.cornell.mannlib.orcidclient.OrcidClientException; +import edu.cornell.mannlib.orcidclient.actions.AddExternalIdAction; +import edu.cornell.mannlib.orcidclient.auth.AuthorizationStatus; +import edu.cornell.mannlib.orcidclient.beans.ExternalId; +import edu.cornell.mannlib.orcidclient.orcidmessage.OrcidMessage; +import edu.cornell.mannlib.vitro.webapp.beans.Individual; +import edu.cornell.mannlib.vitro.webapp.controller.VitroRequest; +import edu.cornell.mannlib.vitro.webapp.controller.freemarker.responsevalues.ResponseValues; + +/** + * We should now be logged in to ORCID and authorized to add an external ID. + */ +public class OrcidAddExternalIdHandler extends OrcidAbstractHandler { + private static final Log log = LogFactory + .getLog(OrcidAddExternalIdHandler.class); + + private AuthorizationStatus status; + private OrcidMessage profile; + + protected OrcidAddExternalIdHandler(VitroRequest vreq) { + super(vreq); + } + + public ResponseValues exec() throws OrcidClientException { + status = auth.getAuthorizationStatus(ADD_EXTERNAL_ID); + if (status.isSuccess()) { + addVivoId(); + return showConfirmationPage(ADDED_ID, profile); + } else if (status.isDenied()) { + return showConfirmationPage(DENIED_ID); + } else { + return showConfirmationPage(FAILED_ID); + } + } + + private void addVivoId() throws OrcidClientException { + Individual individual = findIndividual(); + ExternalId externalId = new ExternalId().setCommonName("VIVO Cornell") + .setReference(individual.getLocalName()) + .setUrl(individual.getURI()).setVisibility(PUBLIC); + + log.debug("Adding external VIVO ID"); + profile = new AddExternalIdAction().execute(externalId, + status.getAccessToken()); + } + +} diff --git a/src/edu/cornell/mannlib/vivo/orcid/controller/OrcidAuthAuthenticateHandler.java b/src/edu/cornell/mannlib/vivo/orcid/controller/OrcidAuthAuthenticateHandler.java new file mode 100644 index 00000000..0a635608 --- /dev/null +++ b/src/edu/cornell/mannlib/vivo/orcid/controller/OrcidAuthAuthenticateHandler.java @@ -0,0 +1,66 @@ +/* $This file is distributed under the terms of the license in /doc/license.txt$ */ + +package edu.cornell.mannlib.vivo.orcid.controller; + +import static edu.cornell.mannlib.orcidclient.actions.ApiAction.AUTHENTICATE; +import static edu.cornell.mannlib.vivo.orcid.controller.OrcidConfirmationState.Progress.DENIED_AUTHENTICATE; +import static edu.cornell.mannlib.vivo.orcid.controller.OrcidConfirmationState.Progress.FAILED_AUTHENTICATE; +import static edu.cornell.mannlib.vivo.orcid.controller.OrcidIntegrationController.PATH_READ_PROFILE; + +import java.net.URISyntaxException; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; + +import edu.cornell.mannlib.orcidclient.OrcidClientException; +import edu.cornell.mannlib.orcidclient.auth.AuthorizationStatus; +import edu.cornell.mannlib.vitro.webapp.controller.VitroRequest; +import edu.cornell.mannlib.vitro.webapp.controller.freemarker.responsevalues.RedirectResponseValues; +import edu.cornell.mannlib.vitro.webapp.controller.freemarker.responsevalues.ResponseValues; + +/** + * We offered the confirmation screen, and they decided to go ahead. Get + * authorization to authenticate them. + * + * We can't assume that they haven't been here before, so they might already + * have authorized, or denied authorization. + */ +public class OrcidAuthAuthenticateHandler extends OrcidAbstractHandler { + private static final Log log = LogFactory + .getLog(OrcidAuthAuthenticateHandler.class); + + private AuthorizationStatus status; + + public OrcidAuthAuthenticateHandler(VitroRequest vreq) { + super(vreq); + } + + public ResponseValues exec() throws URISyntaxException, + OrcidClientException { + status = auth.getAuthorizationStatus(AUTHENTICATE); + if (status.isNone()) { + return seekAuthorizationForAuthenticate(); + } else if (status.isSuccess()) { + return redirectToReadProfile(); + } else if (status.isDenied()) { + return showConfirmationPage(DENIED_AUTHENTICATE); + } else { + return showConfirmationPage(FAILED_AUTHENTICATE); + } + } + + private ResponseValues seekAuthorizationForAuthenticate() + throws OrcidClientException, URISyntaxException { + log.debug("Seeking authorization to authenticate."); + String returnUrl = occ.resolvePathWithWebapp(PATH_READ_PROFILE); + String seekUrl = auth.seekAuthorization(AUTHENTICATE, returnUrl); + return new RedirectResponseValues(seekUrl); + } + + private ResponseValues redirectToReadProfile() throws URISyntaxException { + log.debug("Already authorized to authenticate."); + return new RedirectResponseValues( + occ.resolvePathWithWebapp(PATH_READ_PROFILE)); + } + +} diff --git a/src/edu/cornell/mannlib/vivo/orcid/controller/OrcidAuthExternalIdsHandler.java b/src/edu/cornell/mannlib/vivo/orcid/controller/OrcidAuthExternalIdsHandler.java new file mode 100644 index 00000000..3da78aa1 --- /dev/null +++ b/src/edu/cornell/mannlib/vivo/orcid/controller/OrcidAuthExternalIdsHandler.java @@ -0,0 +1,66 @@ +/* $This file is distributed under the terms of the license in /doc/license.txt$ */ + +package edu.cornell.mannlib.vivo.orcid.controller; + +import static edu.cornell.mannlib.orcidclient.actions.ApiAction.ADD_EXTERNAL_ID; +import static edu.cornell.mannlib.vivo.orcid.controller.OrcidConfirmationState.Progress.DENIED_ID; +import static edu.cornell.mannlib.vivo.orcid.controller.OrcidConfirmationState.Progress.FAILED_ID; +import static edu.cornell.mannlib.vivo.orcid.controller.OrcidIntegrationController.PATH_ADD_EXTERNAL_ID; + +import java.net.URISyntaxException; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; + +import edu.cornell.mannlib.orcidclient.OrcidClientException; +import edu.cornell.mannlib.orcidclient.auth.AuthorizationStatus; +import edu.cornell.mannlib.vitro.webapp.controller.VitroRequest; +import edu.cornell.mannlib.vitro.webapp.controller.freemarker.responsevalues.RedirectResponseValues; +import edu.cornell.mannlib.vitro.webapp.controller.freemarker.responsevalues.ResponseValues; + +/** + * We offered to add external IDs and they decided to go ahead. Get + * authorization. + * + * We can't assume that they haven't been here before, so they might already + * have authorized, or denied authorization. + */ +public class OrcidAuthExternalIdsHandler extends OrcidAbstractHandler { + private static final Log log = LogFactory + .getLog(OrcidAuthExternalIdsHandler.class); + + private AuthorizationStatus status; + + public OrcidAuthExternalIdsHandler(VitroRequest vreq) { + super(vreq); + } + + public ResponseValues exec() throws URISyntaxException, + OrcidClientException { + status = auth.getAuthorizationStatus(ADD_EXTERNAL_ID); + if (status.isNone()) { + return seekAuthorizationForExternalId(); + } else if (status.isSuccess()) { + return redirectToAddExternalId(); + } else if (status.isDenied()) { + return showConfirmationPage(DENIED_ID); + } else { + return showConfirmationPage(FAILED_ID); + } + } + + private ResponseValues seekAuthorizationForExternalId() + throws OrcidClientException, URISyntaxException { + log.debug("Seeking authorization to add external ID"); + String returnUrl = occ.resolvePathWithWebapp(PATH_ADD_EXTERNAL_ID); + String seekUrl = auth.seekAuthorization(ADD_EXTERNAL_ID, returnUrl); + return new RedirectResponseValues(seekUrl); + } + + private ResponseValues redirectToAddExternalId() throws URISyntaxException { + log.debug("Already authorized to add external ID."); + return new RedirectResponseValues( + occ.resolvePathWithWebapp(PATH_ADD_EXTERNAL_ID)); + } + +} diff --git a/src/edu/cornell/mannlib/vivo/orcid/controller/OrcidCallbackHandler.java b/src/edu/cornell/mannlib/vivo/orcid/controller/OrcidCallbackHandler.java new file mode 100644 index 00000000..fc275631 --- /dev/null +++ b/src/edu/cornell/mannlib/vivo/orcid/controller/OrcidCallbackHandler.java @@ -0,0 +1,55 @@ +/* $This file is distributed under the terms of the license in /doc/license.txt$ */ + +package edu.cornell.mannlib.vivo.orcid.controller; + +import static javax.servlet.http.HttpServletResponse.SC_INTERNAL_SERVER_ERROR; + +import java.io.IOException; + +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; + +import edu.cornell.mannlib.orcidclient.OrcidClientException; +import edu.cornell.mannlib.orcidclient.auth.AuthorizationManager; +import edu.cornell.mannlib.orcidclient.auth.AuthorizationStatus; +import edu.cornell.mannlib.orcidclient.context.OrcidClientContext; + +/** + * Handle the callbacks during the OAuth dance. + * + * This is not like other handlers. It is created and invoked from doGet(), not + * from processRequest(). + */ +public class OrcidCallbackHandler { + private static final Log log = LogFactory + .getLog(OrcidCallbackHandler.class); + + private final HttpServletRequest req; + private final HttpServletResponse resp; + + public OrcidCallbackHandler(HttpServletRequest req, HttpServletResponse resp) { + this.req = req; + this.resp = resp; + } + + public void exec() throws IOException { + OrcidClientContext occ = OrcidClientContext.getInstance(); + AuthorizationManager authManager = occ.getAuthorizationManager(req); + try { + AuthorizationStatus auth = authManager + .processAuthorizationResponse(); + if (auth.isSuccess()) { + resp.sendRedirect(auth.getSuccessUrl()); + } else { + resp.sendRedirect(auth.getFailureUrl()); + } + } catch (OrcidClientException e) { + log.error("Invalid authorization response", e); + resp.sendError(SC_INTERNAL_SERVER_ERROR); + } + } + +} diff --git a/src/edu/cornell/mannlib/vivo/orcid/controller/OrcidConfirmationState.java b/src/edu/cornell/mannlib/vivo/orcid/controller/OrcidConfirmationState.java new file mode 100644 index 00000000..e60a841b --- /dev/null +++ b/src/edu/cornell/mannlib/vivo/orcid/controller/OrcidConfirmationState.java @@ -0,0 +1,250 @@ +/* $This file is distributed under the terms of the license in /doc/license.txt$ */ + +package edu.cornell.mannlib.vivo.orcid.controller; + +import static edu.cornell.mannlib.vivo.orcid.controller.OrcidConfirmationState.Progress.START; +import static edu.cornell.mannlib.vivo.orcid.controller.OrcidIntegrationController.PATH_AUTH_EXTERNAL_ID; +import static edu.cornell.mannlib.vivo.orcid.controller.OrcidIntegrationController.PATH_AUTH_AUTHENTICATE; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.EnumSet; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; + +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpSession; +import javax.xml.bind.JAXBElement; +import javax.xml.namespace.QName; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; + +import edu.cornell.mannlib.orcidclient.orcidmessage.ExternalIdentifier; +import edu.cornell.mannlib.orcidclient.orcidmessage.ExternalIdentifiers; +import edu.cornell.mannlib.orcidclient.orcidmessage.OrcidBio; +import edu.cornell.mannlib.orcidclient.orcidmessage.OrcidId; +import edu.cornell.mannlib.orcidclient.orcidmessage.OrcidMessage; +import edu.cornell.mannlib.orcidclient.orcidmessage.OrcidProfile; +import edu.cornell.mannlib.vitro.webapp.controller.freemarker.UrlBuilder; + +/** + * Keep track of where we are in the Orcid confirmation process; what has been + * requested, and what has been returned. + */ +class OrcidConfirmationState { + private static final Log log = LogFactory + .getLog(OrcidConfirmationState.class); + + + // ---------------------------------------------------------------------- + // The factory + // ---------------------------------------------------------------------- + + private static final String ATTRIBUTE_NAME = OrcidConfirmationState.class + .getName(); + + static OrcidConfirmationState fetch(HttpServletRequest req) { + HttpSession session = req.getSession(); + Object o = session.getAttribute(ATTRIBUTE_NAME); + if (o instanceof OrcidConfirmationState) { + return (OrcidConfirmationState) o; + } else { + OrcidConfirmationState ocs = new OrcidConfirmationState(); + session.setAttribute(ATTRIBUTE_NAME, ocs); + return ocs; + } + } + + // ---------------------------------------------------------------------- + // The instance + // ---------------------------------------------------------------------- + + public enum Progress { + START, DENIED_AUTHENTICATE, FAILED_AUTHENTICATE, GOT_PROFILE, ID_ALREADY_PRESENT, DENIED_ID, FAILED_ID, ADDED_ID + } + + private static final Set requiresMessage = EnumSet.of( + Progress.GOT_PROFILE, Progress.ADDED_ID); + + private Progress progress; + private String individualUri; + private Set existingOrcids; + private OrcidMessage profile; + private String profilePageUrl; + + public void reset(String uri, String profileUrl) { + progress = START; + individualUri = uri; + existingOrcids = Collections.emptySet(); + profile = null; + profilePageUrl = profileUrl; + } + + public void setExistingOrcids(Set existing) { + existingOrcids = new HashSet<>(existing); + } + + public void progress(Progress p, OrcidMessage... messages) { + progress = p; + + if (requiresMessage.contains(p)) { + if (messages.length != 1) { + throw new IllegalStateException("Progress to " + p + + " requires an OrcidMessage"); + } + profile = messages[0]; + } else { + if (messages.length != 0) { + throw new IllegalStateException("Progress to " + p + + " does not accept an OrcidMessage"); + } + } + } + + // ---------------------------------------------------------------------- + // Convenience methods for extracting information from the profile. + // ---------------------------------------------------------------------- + + public String getProgress() { + return progress.toString(); + } + + public String getProgressUrl() { + switch (progress) { + case START: + return UrlBuilder.getUrl(PATH_AUTH_AUTHENTICATE); + case GOT_PROFILE: + return UrlBuilder.getUrl(PATH_AUTH_EXTERNAL_ID); + default: + return null; + } + } + + public String getIndividualUri() { + return individualUri; + } + + public String getProfilePageUrl() { + return profilePageUrl; + } + + public String getOrcid() { + return getElementFromOrcidIdentifier("path"); + + } + + public String getOrcidUri() { + return getElementFromOrcidIdentifier("uri"); + } + + public ExternalIdentifier getVivoId() { + for (ExternalIdentifier id : getExternalIds()) { + if (individualUri.equals(id.getExternalIdUrl().getValue())) { + return id; + } + } + return null; + } + + public List getExternalIds() { + OrcidProfile orcidProfile = getOrcidProfile(); + if (orcidProfile == null) { + return Collections.emptyList(); + } + + OrcidBio bio = orcidProfile.getOrcidBio(); + if (bio == null) { + return Collections.emptyList(); + } + + ExternalIdentifiers identifiers = bio.getExternalIdentifiers(); + if (identifiers == null) { + return Collections.emptyList(); + } + + List list = identifiers.getExternalIdentifier(); + if (list == null) { + return Collections.emptyList(); + } + + return list; + } + + private String getElementFromOrcidIdentifier(String elementName) { + OrcidProfile orcidProfile = getOrcidProfile(); + if (orcidProfile == null) { + return ""; + } + + OrcidId id = orcidProfile.getOrcidIdentifier(); + if (id == null) { + log.warn("There is no ORCID Identifier in the profile."); + return ""; + } + + List> idElements = id.getContent(); + if (idElements != null) { + for (JAXBElement idElement : idElements) { + QName name = idElement.getName(); + if (name != null && elementName.equals(name.getLocalPart())) { + String value = idElement.getValue(); + if (value != null) { + return value; + } + } + } + } + log.warn("Didn't find the element '' in the ORCID Identifier: " + idElements); + return ""; + } + + private OrcidProfile getOrcidProfile() { + if (profile == null) { + return null; + } + + OrcidProfile orcidProfile = profile.getOrcidProfile(); + if (orcidProfile == null) { + return null; + } + + return orcidProfile; + } + + public Map toMap() { + Map map = new HashMap<>(); + map.put("progress", progress.toString()); + map.put("individualUri", individualUri); + map.put("profilePage", profilePageUrl); + map.put("orcid", getOrcid()); + map.put("orcidUri", getOrcidUri()); + map.put("hasVivoId", getVivoId() == null); + map.put("externalIds", formatExternalIds()); + map.put("existingOrcids", existingOrcids); + + String progressUrl = getProgressUrl(); + if (progressUrl == null) { + map.put("progressUrl", ""); + } else { + map.put("progressUrl", progressUrl); + } + + return map; + } + + private List> formatExternalIds() { + List> list = new ArrayList<>(); + for (ExternalIdentifier id : getExternalIds()) { + Map map = new HashMap<>(); + map.put("commonName", id.getExternalIdCommonName().getContent()); + map.put("reference", id.getExternalIdReference().getContent()); + map.put("uri", id.getExternalIdUrl().getValue()); + list.add(map); + } + return list; + } +} diff --git a/src/edu/cornell/mannlib/vivo/orcid/controller/OrcidDefaultHandler.java b/src/edu/cornell/mannlib/vivo/orcid/controller/OrcidDefaultHandler.java new file mode 100644 index 00000000..bcacfb5c --- /dev/null +++ b/src/edu/cornell/mannlib/vivo/orcid/controller/OrcidDefaultHandler.java @@ -0,0 +1,126 @@ +/* $This file is distributed under the terms of the license in /doc/license.txt$ */ + +package edu.cornell.mannlib.vivo.orcid.controller; + +import static edu.cornell.mannlib.orcidclient.actions.ApiAction.ADD_EXTERNAL_ID; +import static edu.cornell.mannlib.orcidclient.actions.ApiAction.READ_PROFILE; +import static edu.cornell.mannlib.vivo.orcid.OrcidIdDataGetter.ORCID_ID; +import static javax.servlet.http.HttpServletResponse.SC_BAD_REQUEST; + +import java.util.Collection; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; + +import edu.cornell.mannlib.vedit.beans.LoginStatusBean; +import edu.cornell.mannlib.vitro.webapp.auth.identifier.IdentifierBundle; +import edu.cornell.mannlib.vitro.webapp.auth.identifier.RequestIdentifiers; +import edu.cornell.mannlib.vitro.webapp.auth.identifier.common.HasProfile; +import edu.cornell.mannlib.vitro.webapp.beans.Individual; +import edu.cornell.mannlib.vitro.webapp.beans.ObjectPropertyStatement; +import edu.cornell.mannlib.vitro.webapp.beans.UserAccount; +import edu.cornell.mannlib.vitro.webapp.controller.VitroRequest; +import edu.cornell.mannlib.vitro.webapp.controller.freemarker.UrlBuilder; +import edu.cornell.mannlib.vitro.webapp.controller.freemarker.responsevalues.NotAuthorizedResponseValues; +import edu.cornell.mannlib.vitro.webapp.controller.freemarker.responsevalues.ResponseValues; +import edu.cornell.mannlib.vitro.webapp.controller.freemarker.responsevalues.TemplateResponseValues; + +/** + * A request came from the "Confirm" button on the individual profile. Get a + * fresh state object, clear the AuthorizationCache and show the confirmation + * page. + */ +public class OrcidDefaultHandler extends OrcidAbstractHandler { + private static final Log log = LogFactory.getLog(OrcidDefaultHandler.class); + + private Individual individual; + private final Set existingOrcids = new HashSet<>(); + + public OrcidDefaultHandler(VitroRequest vreq) { + super(vreq); + } + + public ResponseValues exec() { + try { + initializeState(); + initializeAuthorizationCache(); + } catch (Exception e) { + log.error("No proper individual URI on the request", e); + return show400BadRequest(e); + } + + if (!isAuthorized()) { + return showNotAuthorized(); + } + + return showConfirmationPage(); + } + + private void initializeState() { + String uri = vreq.getParameter("individualUri"); + if (uri == null) { + throw new IllegalStateException( + "No 'individualUri' parameter on request."); + } + + String profilePage = UrlBuilder.getIndividualProfileUrl(uri, vreq); + state.reset(uri, profilePage); + + individual = findIndividual(); + locateExistingOrcids(); + state.setExistingOrcids(existingOrcids); + } + + private void locateExistingOrcids() { + if (individual == null) { + return; + } + + List opss = individual + .getObjectPropertyStatements(ORCID_ID); + if (opss == null) { + return; + } + + for (ObjectPropertyStatement ops : opss) { + existingOrcids.add(ops.getObjectURI()); + } + + } + + private void initializeAuthorizationCache() { + auth.clearStatus(READ_PROFILE); + auth.clearStatus(ADD_EXTERNAL_ID); + } + + private ResponseValues show400BadRequest(Exception e) { + Map map = new HashMap<>(); + map.put("title", "400 Bad Request"); + map.put("errorMessage", e.getMessage()); + return new TemplateResponseValues("error-titled.ftl", map, + SC_BAD_REQUEST); + } + + private boolean isAuthorized() { + // Only a self-editor is authorized. + IdentifierBundle ids = RequestIdentifiers.getIdBundleForRequest(vreq); + Collection profileUris = HasProfile.getProfileUris(ids); + log.debug("Authorized? individualUri=" + state.getIndividualUri() + + ", profileUris=" + profileUris); + return profileUris.contains(state.getIndividualUri()); + } + + private ResponseValues showNotAuthorized() { + UserAccount user = LoginStatusBean.getCurrentUser(vreq); + String userName = (user == null) ? "ANONYMOUS" : user.getEmailAddress(); + return new NotAuthorizedResponseValues(userName + + "is not authorized for ORCID operations on '" + individual + + "'"); + } + +} diff --git a/src/edu/cornell/mannlib/vivo/orcid/controller/OrcidIllegalStateException.java b/src/edu/cornell/mannlib/vivo/orcid/controller/OrcidIllegalStateException.java new file mode 100644 index 00000000..164c2396 --- /dev/null +++ b/src/edu/cornell/mannlib/vivo/orcid/controller/OrcidIllegalStateException.java @@ -0,0 +1,20 @@ +/* $This file is distributed under the terms of the license in /doc/license.txt$ */ + +package edu.cornell.mannlib.vivo.orcid.controller; + +import edu.cornell.mannlib.orcidclient.OrcidClientException; + +/** + * The OrcidConfirmationState is not as we expected. Probably deserves a 500 + * error. + */ +public class OrcidIllegalStateException extends OrcidClientException { + public OrcidIllegalStateException(String message) { + super(message); + } + + public OrcidIllegalStateException(String message, Throwable cause) { + super(message, cause); + } + +} diff --git a/src/edu/cornell/mannlib/vivo/orcid/controller/OrcidIntegrationController.java b/src/edu/cornell/mannlib/vivo/orcid/controller/OrcidIntegrationController.java new file mode 100644 index 00000000..e06474d1 --- /dev/null +++ b/src/edu/cornell/mannlib/vivo/orcid/controller/OrcidIntegrationController.java @@ -0,0 +1,140 @@ +/* $This file is distributed under the terms of the license in /doc/license.txt$ */ + +package edu.cornell.mannlib.vivo.orcid.controller; + +import static javax.servlet.http.HttpServletResponse.SC_INTERNAL_SERVER_ERROR; +import static javax.servlet.http.HttpServletResponse.SC_NOT_FOUND; + +import java.io.IOException; + +import javax.servlet.ServletException; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; + +import edu.cornell.mannlib.orcidclient.context.OrcidClientContext; +import edu.cornell.mannlib.orcidclient.context.OrcidClientContext.Setting; +import edu.cornell.mannlib.vitro.webapp.auth.requestedAction.AuthorizationRequest; +import edu.cornell.mannlib.vitro.webapp.controller.VitroRequest; +import edu.cornell.mannlib.vitro.webapp.controller.authenticate.LogoutRedirector; +import edu.cornell.mannlib.vitro.webapp.controller.freemarker.FreemarkerHttpServlet; +import edu.cornell.mannlib.vitro.webapp.controller.freemarker.responsevalues.ExceptionResponseValues; +import edu.cornell.mannlib.vitro.webapp.controller.freemarker.responsevalues.ResponseValues; + +/** + * New workflow: + * + *
+ *    Default: clear status for both readProfile and addExternalIDs 
+ *      show intro screen orcidOffer.ftl
+ *    	The click "do it", goes to /getProfileAuth
+ *      Or "return to profile"
+ *    /getProfileAuth: If already authorized, redirect to /readProfile
+ *      Else, do the dance, ending with /readProfile callback
+ *      Denied? show orcidDenied.ftl
+ *      Failed? show orcidFailed.ftl
+ *    /readProfile: read the profile, store in status 
+ *    	figure external ID options, show orcidOfferIds.ftl
+ *      If they click "do it", goes /authExternalIds
+ *      If they click "nah", return to profile
+ *    /authExternalIds: if already authorized, redirect to /addExternalIds
+ *      Else, do the dance, ending with /addExternalIds callback
+ *    /addExternalIds add one or both IDs, store new profile in status
+ *      show orcidSuccess.ftl with "return to profile" and "view profile" links.
+ * 
+ */ +public class OrcidIntegrationController extends FreemarkerHttpServlet { + private static final Log log = LogFactory + .getLog(OrcidIntegrationController.class); + + private final static String PATHINFO_CALLBACK = "/callback"; + private final static String PATHINFO_AUTH_AUTHENTICATE = "/getAuthticateAuth"; + private final static String PATHINFO_READ_PROFILE = "/readProfile"; + private final static String PATHINFO_AUTH_EXTERNAL_ID = "/authExternalId"; + private final static String PATHINFO_ADD_EXTERNAL_ID = "/addExternalId"; + + public final static String PATH_DEFAULT = "orcid"; + + final static String PATH_AUTH_AUTHENTICATE = path(PATHINFO_AUTH_AUTHENTICATE); + final static String PATH_READ_PROFILE = path(PATHINFO_READ_PROFILE); + final static String PATH_AUTH_EXTERNAL_ID = path(PATHINFO_AUTH_EXTERNAL_ID); + final static String PATH_ADD_EXTERNAL_ID = path(PATHINFO_ADD_EXTERNAL_ID); + + static String path(String pathInfo) { + return PATH_DEFAULT + pathInfo; + } + + final static String TEMPLATE_CONFIRM = "orcidConfirm.ftl"; + + public static final String PROPERTY_EXTERNAL_ID_COMMON_NAME = "orcid.externalIdCommonName"; + public static final String DEFAULT_EXTERNAL_ID_COMMON_NAME = "VIVO Identifier"; + + /** + * Get in before FreemarkerHttpServlet for special handling. + */ + @Override + public void doGet(HttpServletRequest req, HttpServletResponse resp) + throws IOException, ServletException { + if (!isOrcidConfigured()) { + show404NotFound(resp); + } + if (PATHINFO_CALLBACK.equals(req.getPathInfo())) { + new OrcidCallbackHandler(req, resp).exec(); + } else { + super.doGet(req, resp); + } + } + + /** + * We return AUTHORIZED here, but we want the LogoutRedirector to know that + * the user should not remain on this page after logging out. + */ + @Override + protected AuthorizationRequest requiredActions(VitroRequest vreq) { + LogoutRedirector.recordRestrictedPageUri(vreq); + return AuthorizationRequest.AUTHORIZED; + } + + /** + * Look at the path info and delegate to a handler. + */ + @Override + protected ResponseValues processRequest(VitroRequest vreq) throws Exception { + try { + String pathInfo = vreq.getPathInfo(); + log.debug("Path info: " + pathInfo); + if (PATHINFO_AUTH_AUTHENTICATE.equals(pathInfo)) { + return new OrcidAuthAuthenticateHandler(vreq).exec(); + } else if (PATHINFO_READ_PROFILE.equals(pathInfo)) { + return new OrcidReadProfileHandler(vreq).exec(); + } else if (PATHINFO_AUTH_EXTERNAL_ID.equals(pathInfo)) { + return new OrcidAuthExternalIdsHandler(vreq).exec(); + } else if (PATHINFO_ADD_EXTERNAL_ID.equals(pathInfo)) { + return new OrcidAddExternalIdHandler(vreq).exec(); + } else { + return new OrcidDefaultHandler(vreq).exec(); + } + } catch (Exception e) { + return new ExceptionResponseValues(e, SC_INTERNAL_SERVER_ERROR); + } + } + + /** + * If the ORCID interface is configured, it should not throw an exception + * when asked for the value of a setting. + */ + private boolean isOrcidConfigured() { + try { + OrcidClientContext.getInstance().getSetting(Setting.CLIENT_ID); + return true; + } catch (Exception e) { + return false; + } + } + + private void show404NotFound(HttpServletResponse resp) throws IOException { + resp.sendError(SC_NOT_FOUND); + } +} diff --git a/src/edu/cornell/mannlib/vivo/orcid/controller/OrcidReadProfileHandler.java b/src/edu/cornell/mannlib/vivo/orcid/controller/OrcidReadProfileHandler.java new file mode 100644 index 00000000..1e857429 --- /dev/null +++ b/src/edu/cornell/mannlib/vivo/orcid/controller/OrcidReadProfileHandler.java @@ -0,0 +1,61 @@ +/* $This file is distributed under the terms of the license in /doc/license.txt$ */ + +package edu.cornell.mannlib.vivo.orcid.controller; + +import static edu.cornell.mannlib.orcidclient.actions.ApiAction.AUTHENTICATE; +import static edu.cornell.mannlib.vivo.orcid.controller.OrcidConfirmationState.Progress.DENIED_AUTHENTICATE; +import static edu.cornell.mannlib.vivo.orcid.controller.OrcidConfirmationState.Progress.FAILED_AUTHENTICATE; +import static edu.cornell.mannlib.vivo.orcid.controller.OrcidConfirmationState.Progress.GOT_PROFILE; +import static edu.cornell.mannlib.vivo.orcid.controller.OrcidConfirmationState.Progress.ID_ALREADY_PRESENT; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; + +import edu.cornell.mannlib.orcidclient.OrcidClientException; +import edu.cornell.mannlib.orcidclient.actions.ReadPublicBioAction; +import edu.cornell.mannlib.orcidclient.auth.AuthorizationStatus; +import edu.cornell.mannlib.orcidclient.orcidmessage.OrcidMessage; +import edu.cornell.mannlib.vitro.webapp.controller.VitroRequest; +import edu.cornell.mannlib.vitro.webapp.controller.freemarker.responsevalues.ResponseValues; + +/** + * We should now know the user's ORCID, so read the user's public ORCID profile. + */ +public class OrcidReadProfileHandler extends OrcidAbstractHandler { + private static final Log log = LogFactory + .getLog(OrcidReadProfileHandler.class); + + private AuthorizationStatus status; + private OrcidMessage profile; + + protected OrcidReadProfileHandler(VitroRequest vreq) { + super(vreq); + } + + public ResponseValues exec() throws OrcidClientException { + status = auth.getAuthorizationStatus(AUTHENTICATE); + if (status.isSuccess()) { + readProfile(); + state.progress(GOT_PROFILE, profile); + + recordConfirmation(); + + if (state.getVivoId() != null) { + state.progress(ID_ALREADY_PRESENT); + } + + return showConfirmationPage(); + } else if (status.isDenied()) { + return showConfirmationPage(DENIED_AUTHENTICATE); + } else { + return showConfirmationPage(FAILED_AUTHENTICATE); + } + } + + private void readProfile() throws OrcidClientException { + profile = new ReadPublicBioAction().execute(status.getAccessToken() + .getOrcid()); + log.debug("Read profile"); + } + +} diff --git a/themes/wilma/templates/individual--foaf-person.ftl b/themes/wilma/templates/individual--foaf-person.ftl index 618b96bf..c6bf872f 100644 --- a/themes/wilma/templates/individual--foaf-person.ftl +++ b/themes/wilma/templates/individual--foaf-person.ftl @@ -21,6 +21,10 @@ <#assign languageCount = 1> <#assign visRequestingTemplate = "foaf-person-wilma"> + +<#--add the VIVO-ORCID interface --> +<#include "individual-orcidInterface.ftl"> +
diff --git a/utilities/orcid/mockorcid.war b/utilities/orcid/mockorcid.war new file mode 100644 index 00000000..64bd77a1 Binary files /dev/null and b/utilities/orcid/mockorcid.war differ