From 91b6b70b3b4becac16dcd29b48c3a9553a70bcde Mon Sep 17 00:00:00 2001 From: ajay3568yadav Date: Wed, 16 Jul 2025 15:10:10 -0700 Subject: [PATCH 01/31] VSPC-189 added citeshpere connection --- .../core/services/CitesphereAuthToken.java | 76 ++++++ .../core/services/ICitesphereManager.java | 104 ++++++++ .../core/services/impl/CitesphereManager.java | 248 ++++++++++++++++++ .../views/staff/modules/slides/slide.html | 168 +++++++++--- 4 files changed, 555 insertions(+), 41 deletions(-) create mode 100644 vspace/src/main/java/edu/asu/diging/vspace/core/services/CitesphereAuthToken.java create mode 100644 vspace/src/main/java/edu/asu/diging/vspace/core/services/ICitesphereManager.java create mode 100644 vspace/src/main/java/edu/asu/diging/vspace/core/services/impl/CitesphereManager.java diff --git a/vspace/src/main/java/edu/asu/diging/vspace/core/services/CitesphereAuthToken.java b/vspace/src/main/java/edu/asu/diging/vspace/core/services/CitesphereAuthToken.java new file mode 100644 index 000000000..f0ffb4ffd --- /dev/null +++ b/vspace/src/main/java/edu/asu/diging/vspace/core/services/CitesphereAuthToken.java @@ -0,0 +1,76 @@ +package edu.asu.diging.vspace.core.services; + +import java.util.Map; + +/** + * Authentication token object for Citesphere API + */ +public class CitesphereAuthToken { + + private String authType; + private Map headers; + private String accessToken; + private String username; + private String password; + + /** + * Constructor for OAuth authentication + * @param accessToken OAuth access token + */ + public CitesphereAuthToken(String accessToken) { + this.authType = "oauth"; + this.accessToken = accessToken; + } + + /** + * Constructor for Basic authentication + * @param username Username + * @param password Password + */ + public CitesphereAuthToken(String username, String password) { + this.authType = "basic"; + this.username = username; + this.password = password; + } + + // Getters and setters + public String getAuthType() { + return authType; + } + + public void setAuthType(String authType) { + this.authType = authType; + } + + public Map getHeaders() { + return headers; + } + + public void setHeaders(Map headers) { + this.headers = headers; + } + + public String getAccessToken() { + return accessToken; + } + + public void setAccessToken(String accessToken) { + this.accessToken = accessToken; + } + + public String getUsername() { + return username; + } + + public void setUsername(String username) { + this.username = username; + } + + public String getPassword() { + return password; + } + + public void setPassword(String password) { + this.password = password; + } +} \ No newline at end of file diff --git a/vspace/src/main/java/edu/asu/diging/vspace/core/services/ICitesphereManager.java b/vspace/src/main/java/edu/asu/diging/vspace/core/services/ICitesphereManager.java new file mode 100644 index 000000000..06d6b16f1 --- /dev/null +++ b/vspace/src/main/java/edu/asu/diging/vspace/core/services/ICitesphereManager.java @@ -0,0 +1,104 @@ +package edu.asu.diging.vspace.core.services; + +import java.util.Map; + +/** + * Service interface for Citesphere API operations + */ +public interface ICitesphereManager { + + /** + * Get user information + * @return User data as Map + */ + Map getUser(); + + /** + * Check test endpoint + * @return Test response as Map + */ + Map checkTest(); + + /** + * Check access for a document + * @param documentId Document ID to check access for + * @return Access check response as Map + */ + Map checkAccess(String documentId); + + /** + * Get data by endpoint + * @param endpoint API endpoint path + * @return Data response as Map + */ + Map getDataByEndpoint(String endpoint); + + /** + * Get all groups + * @return Groups data as Map + */ + Map getGroups(); + + /** + * Get group information + * @param groupId Group ID + * @return Group information as Map + */ + Map getGroupInfo(String groupId); + + /** + * Get group items + * @param zoteroGroupId Zotero group ID + * @return Group items as Map + */ + Map getGroupItems(String zoteroGroupId); + + /** + * Get collections + * @param zoteroGroupId Zotero group ID + * @return Collections as Map + */ + Map getCollections(String zoteroGroupId); + + /** + * Get collection items + * @param zoteroGroupId Zotero group ID + * @param collectionId Collection ID + * @param pageNumber Page number (optional, defaults to 0) + * @return Collection items as Map + */ + Map getCollectionItems(String zoteroGroupId, String collectionId, int pageNumber); + + /** + * Get collection items (overloaded method without page number) + * @param zoteroGroupId Zotero group ID + * @param collectionId Collection ID + * @return Collection items as Map + */ + Map getCollectionItems(String zoteroGroupId, String collectionId); + + /** + * Get item information + * @param zoteroGroupId Zotero group ID + * @param itemId Item ID + * @return Item information as Map + */ + Map getItemInfo(String zoteroGroupId, String itemId); + + /** + * Get collections by collection ID + * @param zoteroGroupId Zotero group ID + * @param collectionId Collection ID + * @return Collections as Map + */ + Map getCollectionsByCollectionId(String zoteroGroupId, String collectionId); + + /** + * Add item to group + * @param groupId Group ID + * @param data Item data + * @param filePath Path to file to upload + * @return Response from API + */ + Object addItem(String groupId, Map data, String filePath); +} diff --git a/vspace/src/main/java/edu/asu/diging/vspace/core/services/impl/CitesphereManager.java b/vspace/src/main/java/edu/asu/diging/vspace/core/services/impl/CitesphereManager.java new file mode 100644 index 000000000..8b948578e --- /dev/null +++ b/vspace/src/main/java/edu/asu/diging/vspace/core/services/impl/CitesphereManager.java @@ -0,0 +1,248 @@ +package edu.asu.diging.vspace.core.services.impl; + + +//import com.citesphere.api.CitesphereService; +//import com.citesphere.api.auth.CitesphereAuthToken; +import com.fasterxml.jackson.databind.ObjectMapper; + +import edu.asu.diging.vspace.core.services.CitesphereAuthToken; +import edu.asu.diging.vspace.core.services.ICitesphereManager; +import okhttp3.*; + +import java.io.File; +import java.io.IOException; +import java.util.Base64; +import java.util.HashMap; +import java.util.Map; + +/** + * Implementation of CitesphereService for API operations + */ +public class CitesphereManager implements ICitesphereManager { + + private final String api; + private final CitesphereAuthToken authTokenObject; + private final OkHttpClient client; + private final ObjectMapper objectMapper; + + /** + * Constructor + * @param api API base URL + * @param authTokenObject Authentication token object + * @return + */ + public void CitesphereConnectorImpl(String api, CitesphereAuthToken authTokenObject) { + this.api = api; + this.authTokenObject = authTokenObject; + this.client = new OkHttpClient(); + this.objectMapper = new ObjectMapper(); + + validate(); + handleApiParams(); + } + + /** + * Validate authentication token object + */ + private void validate() { + if (authTokenObject.getAuthType() == null) { + throw new IllegalArgumentException("Missing authType attribute"); + } + + if (authTokenObject.getAccessToken() == null) { + if (authTokenObject.getUsername() == null || authTokenObject.getPassword() == null) { + throw new IllegalArgumentException( + "Either username and password or access_token should be present"); + } + } + + if (!"oauth".equals(authTokenObject.getAuthType()) && + !"basic".equals(authTokenObject.getAuthType())) { + throw new IllegalArgumentException("authType should be either oauth or basic"); + } + } + + /** + * Handle API parameters and set headers + */ + private void handleApiParams() { + Map headers = new HashMap<>(); + + if ("oauth".equals(authTokenObject.getAuthType())) { + headers.put("Authorization", "Bearer " + authTokenObject.getAccessToken()); + } else if ("basic".equals(authTokenObject.getAuthType())) { + String authStr = authTokenObject.getUsername() + ":" + authTokenObject.getPassword(); + String authB64 = Base64.getEncoder().encodeToString(authStr.getBytes()); + headers.put("Authorization", "Basic " + authB64); + } + + authTokenObject.setHeaders(headers); + } + + /** + * Execute GET command + * @param url Request URL + * @return Response data as Map + */ + private Map executeCommand(String url) { + try { + Request.Builder requestBuilder = new Request.Builder().url(url); + + // Add headers + if (authTokenObject.getHeaders() != null) { + for (Map.Entry header : authTokenObject.getHeaders().entrySet()) { + requestBuilder.addHeader(header.getKey(), header.getValue()); + } + } + + Request request = requestBuilder.build(); + + try (Response response = client.newCall(request).execute()) { + if (response.body() != null) { + String responseBody = response.body().string(); + return objectMapper.readValue(responseBody, Map.class); + } + } + } catch (Exception e) { + Map errorMap = new HashMap<>(); + errorMap.put("error_message", e.getMessage()); + return errorMap; + } + + return new HashMap<>(); + } + + /** + * Execute POST request + * @param url Request URL + * @param data Request data + * @param filePath File path for upload + * @return Response object + */ + private Object executePostRequest(String url, Map data, String filePath) { + try { + MultipartBody.Builder builder = new MultipartBody.Builder() + .setType(MultipartBody.FORM); + + // Add data parameters + if (data != null) { + for (Map.Entry entry : data.entrySet()) { + builder.addFormDataPart(entry.getKey(), entry.getValue().toString()); + } + } + + // Add file if provided + if (filePath != null) { + File file = new File(filePath); + if (file.exists()) { + RequestBody fileBody = RequestBody.create(file, MediaType.parse("application/pdf")); + builder.addFormDataPart("files", file.getName(), fileBody); + } + } + + RequestBody requestBody = builder.build(); + + Request.Builder requestBuilder = new Request.Builder() + .url(url) + .post(requestBody); + + // Add headers + if (authTokenObject.getHeaders() != null) { + for (Map.Entry header : authTokenObject.getHeaders().entrySet()) { + requestBuilder.addHeader(header.getKey(), header.getValue()); + } + } + + Request request = requestBuilder.build(); + + try (Response response = client.newCall(request).execute()) { + System.out.println("Response code: " + response.code()); + return response; + } + + } catch (Exception e) { + System.err.println("[ERROR] -------- Error during API request with " + filePath + ": " + e.getMessage()); + return "Error loading/reading file"; + } + } + + @Override + public Map getUser() { + String url = api + "/v1/user"; + return executeCommand(url); + } + + @Override + public Map checkTest() { + String url = api + "/v1/test"; + return executeCommand(url); + } + + @Override + public Map checkAccess(String documentId) { + String url = api + "/files/giles/" + documentId + "/access/check"; + return executeCommand(url); + } + + @Override + public Map getDataByEndpoint(String endpoint) { + String url = api + "/v1" + endpoint; + return executeCommand(url); + } + + @Override + public Map getGroups() { + String url = api + "/v1/groups"; + return executeCommand(url); + } + + @Override + public Map getGroupInfo(String groupId) { + String url = api + "/v1/groups/" + groupId; + return executeCommand(url); + } + + @Override + public Map getGroupItems(String zoteroGroupId) { + String url = api + "/v1/groups/" + zoteroGroupId + "/items"; + return executeCommand(url); + } + + @Override + public Map getCollections(String zoteroGroupId) { + String url = api + "/v1/groups/" + zoteroGroupId + "/collections"; + return executeCommand(url); + } + + @Override + public Map getCollectionItems(String zoteroGroupId, String collectionId, int pageNumber) { + String url = api + "/v1/groups/" + zoteroGroupId + "/collections/" + collectionId + "/items"; + if (pageNumber > 0) { + url += "?&page=" + pageNumber; + } + return executeCommand(url); + } + + @Override + public Map getCollectionItems(String zoteroGroupId, String collectionId) { + return getCollectionItems(zoteroGroupId, collectionId, 0); + } + + @Override + public Map getItemInfo(String zoteroGroupId, String itemId) { + String url = api + "/v1/groups/" + zoteroGroupId + "/items/" + itemId; + return executeCommand(url); + } + + @Override + public Map getCollectionsByCollectionId(String zoteroGroupId, String collectionId) { + String url = api + "/groups/" + zoteroGroupId + "/collections/" + collectionId + "/collections"; + return executeCommand(url); + } + + @Override + public Object addItem(String groupId, Map data, String filePath) { + String url = api + "/v1/groups/" + groupId + "/items/create"; + return executePostRequest(url, data, filePath); + } +} diff --git a/vspace/src/main/webapp/WEB-INF/views/staff/modules/slides/slide.html b/vspace/src/main/webapp/WEB-INF/views/staff/modules/slides/slide.html index d1708279e..0ef0a1d56 100644 --- a/vspace/src/main/webapp/WEB-INF/views/staff/modules/slides/slide.html +++ b/vspace/src/main/webapp/WEB-INF/views/staff/modules/slides/slide.html @@ -3092,47 +3092,133 @@ - + - + + From 5e639072a8106e236e9a6034902ee89ebf3dce6c Mon Sep 17 00:00:00 2001 From: ajay3568yadav Date: Fri, 8 Aug 2025 16:40:26 -0700 Subject: [PATCH 07/31] VSPC-189 filed selection fix --- .../views/staff/modules/slides/slide.html | 100 ------------------ 1 file changed, 100 deletions(-) diff --git a/vspace/src/main/webapp/WEB-INF/views/staff/modules/slides/slide.html b/vspace/src/main/webapp/WEB-INF/views/staff/modules/slides/slide.html index 5c7317dc8..1c1feb1e9 100644 --- a/vspace/src/main/webapp/WEB-INF/views/staff/modules/slides/slide.html +++ b/vspace/src/main/webapp/WEB-INF/views/staff/modules/slides/slide.html @@ -3183,106 +3183,6 @@ - - -
- - - - - -
-
- - -
- - - - - -
-
- - -
- - - - - -
-
- - -
- - - - - -
-
- - -
- - - - - -
-
- - -
- - - - - -
-
- - -
- - - - - -
-
- - -
- - - - - -
-
- - -
- - - - - -
-
- - -
- - - - - -
-
From 179306f372b80489d2ab949a4ae5367ec9b01ed6 Mon Sep 17 00:00:00 2001 From: ajay3568yadav Date: Mon, 11 Aug 2025 16:17:05 -0700 Subject: [PATCH 08/31] VSPC-189 citesphere manager --- .../core/services/impl/CitesphereManager.java | 322 ++++++++++++------ .../views/staff/modules/slides/slide.html | 100 ++++++ 2 files changed, 313 insertions(+), 109 deletions(-) diff --git a/vspace/src/main/java/edu/asu/diging/vspace/core/services/impl/CitesphereManager.java b/vspace/src/main/java/edu/asu/diging/vspace/core/services/impl/CitesphereManager.java index 9c90a3854..e24f00c35 100644 --- a/vspace/src/main/java/edu/asu/diging/vspace/core/services/impl/CitesphereManager.java +++ b/vspace/src/main/java/edu/asu/diging/vspace/core/services/impl/CitesphereManager.java @@ -1,10 +1,6 @@ package edu.asu.diging.vspace.core.services.impl; - -//import com.citesphere.api.CitesphereService; -//import com.citesphere.api.auth.CitesphereAuthToken; import com.fasterxml.jackson.databind.ObjectMapper; - import edu.asu.diging.vspace.core.services.CitesphereAuthToken; import edu.asu.diging.vspace.core.services.ICitesphereManager; @@ -13,215 +9,310 @@ import java.util.Base64; import java.util.HashMap; import java.util.Map; +import java.util.logging.Logger; +import java.util.logging.Level; + import okhttp3.*; /** - * Implementation of CitesphereService for API operations + * Implementation of ICitesphereManager for Citesphere API operations. + * + * This class handles authentication, HTTP requests, and data management + * for interactions with the Citesphere API service. + * + * @author ASU Digital Innovation Group + * @version 1.0 */ public class CitesphereManager implements ICitesphereManager { - private final String api; + private static final Logger LOGGER = Logger.getLogger(CitesphereManager.class.getName()); + + private static final String OAUTH_AUTH_TYPE = "oauth"; + private static final String BASIC_AUTH_TYPE = "basic"; + private static final String AUTHORIZATION_HEADER = "Authorization"; + private static final String BEARER_PREFIX = "Bearer "; + private static final String BASIC_PREFIX = "Basic "; + private static final String API_VERSION_PATH = "/v1"; + + private final String apiBaseUrl; private final CitesphereAuthToken authTokenObject; - private final OkHttpClient client; + private final OkHttpClient httpClient; private final ObjectMapper objectMapper; /** - * Constructor - * @param api API base URL - * @param authTokenObject Authentication token object - * @return + * Constructor for CitesphereManager. + * + * @param apiBaseUrl The base URL for the Citesphere API + * @param authTokenObject Authentication token object containing credentials + * @throws IllegalArgumentException if authentication parameters are invalid */ - public CitesphereManager(String api, CitesphereAuthToken authTokenObject) { - this.api = api; + public CitesphereManager(String apiBaseUrl, CitesphereAuthToken authTokenObject) { + this.apiBaseUrl = apiBaseUrl; this.authTokenObject = authTokenObject; - this.client = new OkHttpClient(); + this.httpClient = new OkHttpClient(); this.objectMapper = new ObjectMapper(); - validate(); - handleApiParams(); + validateAuthenticationParameters(); + configureAuthenticationHeaders(); } - /** - * Validate authentication token object + * Validates the authentication token object parameters. + * + * @throws IllegalArgumentException if authentication parameters are missing or invalid */ - private void validate() { + private void validateAuthenticationParameters() { if (authTokenObject.getAuthType() == null) { - throw new IllegalArgumentException("Missing authType attribute"); + throw new IllegalArgumentException("Authentication type (authType) is required"); } if (authTokenObject.getAccessToken() == null) { if (authTokenObject.getUsername() == null || authTokenObject.getPassword() == null) { throw new IllegalArgumentException( - "Either username and password or access_token should be present"); + "Either access token or username/password combination is required"); } } - if (!"oauth".equals(authTokenObject.getAuthType()) && - !"basic".equals(authTokenObject.getAuthType())) { - throw new IllegalArgumentException("authType should be either oauth or basic"); + if (!OAUTH_AUTH_TYPE.equals(authTokenObject.getAuthType()) && + !BASIC_AUTH_TYPE.equals(authTokenObject.getAuthType())) { + throw new IllegalArgumentException( + "Authentication type must be either '" + OAUTH_AUTH_TYPE + "' or '" + BASIC_AUTH_TYPE + "'"); } } /** - * Handle API parameters and set headers + * Configures authentication headers based on the authentication type. */ - private void handleApiParams() { + private void configureAuthenticationHeaders() { Map headers = new HashMap<>(); - if ("oauth".equals(authTokenObject.getAuthType())) { - headers.put("Authorization", "Bearer " + authTokenObject.getAccessToken()); - } else if ("basic".equals(authTokenObject.getAuthType())) { - String authStr = authTokenObject.getUsername() + ":" + authTokenObject.getPassword(); - String authB64 = Base64.getEncoder().encodeToString(authStr.getBytes()); - headers.put("Authorization", "Basic " + authB64); + if (OAUTH_AUTH_TYPE.equals(authTokenObject.getAuthType())) { + headers.put(AUTHORIZATION_HEADER, BEARER_PREFIX + authTokenObject.getAccessToken()); + } else if (BASIC_AUTH_TYPE.equals(authTokenObject.getAuthType())) { + String credentials = authTokenObject.getUsername() + ":" + authTokenObject.getPassword(); + String encodedCredentials = Base64.getEncoder().encodeToString(credentials.getBytes()); + headers.put(AUTHORIZATION_HEADER, BASIC_PREFIX + encodedCredentials); } authTokenObject.setHeaders(headers); } /** - * Execute GET command - * @param url Request URL - * @return Response data as Map + * Executes a GET request to the specified URL. + * + * @param url The target URL for the GET request + * @return Response data as a Map, or error information if the request fails */ - private Map executeCommand(String url) { + private Map executeGetRequest(String url) { try { Request.Builder requestBuilder = new Request.Builder().url(url); - // Add headers - if (authTokenObject.getHeaders() != null) { - for (Map.Entry header : authTokenObject.getHeaders().entrySet()) { - requestBuilder.addHeader(header.getKey(), header.getValue()); - } - } + // Add authentication headers + addAuthenticationHeaders(requestBuilder); Request request = requestBuilder.build(); - try (Response response = client.newCall(request).execute()) { - if (response.body() != null) { - String responseBody = response.body().string(); - return objectMapper.readValue(responseBody, Map.class); - } + try (Response response = httpClient.newCall(request).execute()) { + return parseJsonResponse(response); } + } catch (IOException e) { + LOGGER.log(Level.SEVERE, "Error executing GET request to " + url, e); + return createErrorResponse("Network error: " + e.getMessage()); } catch (Exception e) { - Map errorMap = new HashMap<>(); - errorMap.put("error_message", e.getMessage()); - return errorMap; + LOGGER.log(Level.SEVERE, "Unexpected error during GET request to " + url, e); + return createErrorResponse("Unexpected error: " + e.getMessage()); + } + } + + /** + * Adds authentication headers to the request builder. + * + * @param requestBuilder The request builder to add headers to + */ + private void addAuthenticationHeaders(Request.Builder requestBuilder) { + if (authTokenObject.getHeaders() != null) { + authTokenObject.getHeaders().forEach(requestBuilder::addHeader); + } + } + + /** + * Parses JSON response from HTTP response. + * + * @param response The HTTP response to parse + * @return Parsed JSON as Map + * @throws IOException if parsing fails + */ + @SuppressWarnings("unchecked") + private Map parseJsonResponse(Response response) throws IOException { + if (response.body() != null) { + String responseBody = response.body().string(); + if (!responseBody.isEmpty()) { + return objectMapper.readValue(responseBody, Map.class); + } } - return new HashMap<>(); } /** - * Execute POST request - * @param url Request URL - * @param data Request data - * @param filePath File path for upload - * @return Response object + * Creates an error response map. + * + * @param errorMessage The error message to include + * @return Map containing error information + */ + private Map createErrorResponse(String errorMessage) { + Map errorResponse = new HashMap<>(); + errorResponse.put("error", true); + errorResponse.put("error_message", errorMessage); + return errorResponse; + } + + /** + * Executes a POST request with optional file upload. + * + * @param url The target URL for the POST request + * @param data Form data to include in the request + * @param filePath Path to file for upload (optional) + * @return HTTP Response object or error message */ private Object executePostRequest(String url, Map data, String filePath) { try { - MultipartBody.Builder builder = new MultipartBody.Builder() + MultipartBody.Builder formBuilder = new MultipartBody.Builder() .setType(MultipartBody.FORM); - // Add data parameters - if (data != null) { - for (Map.Entry entry : data.entrySet()) { - builder.addFormDataPart(entry.getKey(), entry.getValue().toString()); - } - } + // Add form data parameters + addFormDataParameters(formBuilder, data); - // Add file if provided - if (filePath != null) { - File file = new File(filePath); - if (file.exists()) { - RequestBody fileBody = RequestBody.create(file, MediaType.parse("application/pdf")); - builder.addFormDataPart("files", file.getName(), fileBody); - } - } - - RequestBody requestBody = builder.build(); + // Add file if specified + addFileToForm(formBuilder, filePath); + RequestBody requestBody = formBuilder.build(); Request.Builder requestBuilder = new Request.Builder() .url(url) .post(requestBody); - // Add headers - if (authTokenObject.getHeaders() != null) { - for (Map.Entry header : authTokenObject.getHeaders().entrySet()) { - requestBuilder.addHeader(header.getKey(), header.getValue()); - } - } - + addAuthenticationHeaders(requestBuilder); Request request = requestBuilder.build(); - try (Response response = client.newCall(request).execute()) { - System.out.println("Response code: " + response.code()); + try (Response response = httpClient.newCall(request).execute()) { + LOGGER.info("POST request completed with response code: " + response.code()); return response; } + } catch (IOException e) { + LOGGER.log(Level.SEVERE, "IO error during POST request to " + url, e); + return "IO error during file upload: " + e.getMessage(); } catch (Exception e) { - System.err.println("[ERROR] -------- Error during API request with " + filePath + ": " + e.getMessage()); - return "Error loading/reading file"; + LOGGER.log(Level.SEVERE, "Unexpected error during POST request to " + url, e); + return "Unexpected error during request: " + e.getMessage(); } } + /** + * Adds form data parameters to the multipart form builder. + * + * @param formBuilder The form builder to add parameters to + * @param data The data parameters to add + */ + private void addFormDataParameters(MultipartBody.Builder formBuilder, Map data) { + if (data != null) { + data.forEach((key, value) -> + formBuilder.addFormDataPart(key, String.valueOf(value)) + ); + } + } + + /** + * Adds a file to the multipart form if the file path is provided and valid. + * + * @param formBuilder The form builder to add the file to + * @param filePath The path to the file to upload + */ + private void addFileToForm(MultipartBody.Builder formBuilder, String filePath) { + if (filePath != null && !filePath.trim().isEmpty()) { + File file = new File(filePath); + if (file.exists() && file.isFile()) { + RequestBody fileBody = RequestBody.create(file, MediaType.parse("application/pdf")); + formBuilder.addFormDataPart("files", file.getName(), fileBody); + } else { + LOGGER.warning("File not found or invalid: " + filePath); + } + } + } + + // API endpoint methods + @Override public Map getUser() { - String url = api + "/v1/user"; - return executeCommand(url); + return executeGetRequest(apiBaseUrl + API_VERSION_PATH + "/user"); } @Override public Map checkTest() { - String url = api + "/v1/test"; - return executeCommand(url); + return executeGetRequest(apiBaseUrl + API_VERSION_PATH + "/test"); } @Override public Map checkAccess(String documentId) { - String url = api + "/files/giles/" + documentId + "/access/check"; - return executeCommand(url); + if (documentId == null || documentId.trim().isEmpty()) { + return createErrorResponse("Document ID is required"); + } + return executeGetRequest(apiBaseUrl + "/files/giles/" + documentId + "/access/check"); } @Override public Map getDataByEndpoint(String endpoint) { - String url = api + "/v1" + endpoint; - return executeCommand(url); + if (endpoint == null || endpoint.trim().isEmpty()) { + return createErrorResponse("Endpoint is required"); + } + return executeGetRequest(apiBaseUrl + API_VERSION_PATH + endpoint); } @Override public Map getGroups() { - String url = api + "/v1/groups"; - return executeCommand(url); + return executeGetRequest(apiBaseUrl + API_VERSION_PATH + "/groups"); } @Override public Map getGroupInfo(String groupId) { - String url = api + "/v1/groups/" + groupId; - return executeCommand(url); + if (groupId == null || groupId.trim().isEmpty()) { + return createErrorResponse("Group ID is required"); + } + return executeGetRequest(apiBaseUrl + API_VERSION_PATH + "/groups/" + groupId); } @Override public Map getGroupItems(String zoteroGroupId) { - String url = api + "/v1/groups/" + zoteroGroupId + "/items"; - return executeCommand(url); + if (zoteroGroupId == null || zoteroGroupId.trim().isEmpty()) { + return createErrorResponse("Zotero Group ID is required"); + } + return executeGetRequest(apiBaseUrl + API_VERSION_PATH + "/groups/" + zoteroGroupId + "/items"); } @Override public Map getCollections(String zoteroGroupId) { - String url = api + "/v1/groups/" + zoteroGroupId + "/collections"; - return executeCommand(url); + if (zoteroGroupId == null || zoteroGroupId.trim().isEmpty()) { + return createErrorResponse("Zotero Group ID is required"); + } + return executeGetRequest(apiBaseUrl + API_VERSION_PATH + "/groups/" + zoteroGroupId + "/collections"); } @Override public Map getCollectionItems(String zoteroGroupId, String collectionId, int pageNumber) { - String url = api + "/v1/groups/" + zoteroGroupId + "/collections/" + collectionId + "/items"; + if (zoteroGroupId == null || zoteroGroupId.trim().isEmpty()) { + return createErrorResponse("Zotero Group ID is required"); + } + if (collectionId == null || collectionId.trim().isEmpty()) { + return createErrorResponse("Collection ID is required"); + } + + String url = apiBaseUrl + API_VERSION_PATH + "/groups/" + zoteroGroupId + + "/collections/" + collectionId + "/items"; + if (pageNumber > 0) { - url += "?&page=" + pageNumber; + url += "?page=" + pageNumber; } - return executeCommand(url); + + return executeGetRequest(url); } @Override @@ -231,19 +322,32 @@ public Map getCollectionItems(String zoteroGroupId, String colle @Override public Map getItemInfo(String zoteroGroupId, String itemId) { - String url = api + "/v1/groups/" + zoteroGroupId + "/items/" + itemId; - return executeCommand(url); + if (zoteroGroupId == null || zoteroGroupId.trim().isEmpty()) { + return createErrorResponse("Zotero Group ID is required"); + } + if (itemId == null || itemId.trim().isEmpty()) { + return createErrorResponse("Item ID is required"); + } + return executeGetRequest(apiBaseUrl + API_VERSION_PATH + "/groups/" + zoteroGroupId + "/items/" + itemId); } @Override public Map getCollectionsByCollectionId(String zoteroGroupId, String collectionId) { - String url = api + "/groups/" + zoteroGroupId + "/collections/" + collectionId + "/collections"; - return executeCommand(url); + if (zoteroGroupId == null || zoteroGroupId.trim().isEmpty()) { + return createErrorResponse("Zotero Group ID is required"); + } + if (collectionId == null || collectionId.trim().isEmpty()) { + return createErrorResponse("Collection ID is required"); + } + return executeGetRequest(apiBaseUrl + "/groups/" + zoteroGroupId + "/collections/" + collectionId + "/collections"); } @Override public Object addItem(String groupId, Map data, String filePath) { - String url = api + "/v1/groups/" + groupId + "/items/create"; + if (groupId == null || groupId.trim().isEmpty()) { + return "Group ID is required"; + } + String url = apiBaseUrl + API_VERSION_PATH + "/groups/" + groupId + "/items/create"; return executePostRequest(url, data, filePath); } -} +} \ No newline at end of file diff --git a/vspace/src/main/webapp/WEB-INF/views/staff/modules/slides/slide.html b/vspace/src/main/webapp/WEB-INF/views/staff/modules/slides/slide.html index 1c1feb1e9..5c7317dc8 100644 --- a/vspace/src/main/webapp/WEB-INF/views/staff/modules/slides/slide.html +++ b/vspace/src/main/webapp/WEB-INF/views/staff/modules/slides/slide.html @@ -3183,6 +3183,106 @@ + + +
+ + + + + +
+
+ + +
+ + + + + +
+
+ + +
+ + + + + +
+
+ + +
+ + + + + +
+
+ + +
+ + + + + +
+
+ + +
+ + + + + +
+
+ + +
+ + + + + +
+
+ + +
+ + + + + +
+
+ + +
+ + + + + +
+
+ + +
+ + + + + +
+
From 07c444e3c2994928c48ebdf62ad83625bd882820 Mon Sep 17 00:00:00 2001 From: ajay3568yadav Date: Tue, 12 Aug 2025 16:30:26 -0700 Subject: [PATCH 09/31] VSPC-189 citation manager fixes --- .../core/services/impl/CitesphereManager.java | 322 ++++++------------ 1 file changed, 109 insertions(+), 213 deletions(-) diff --git a/vspace/src/main/java/edu/asu/diging/vspace/core/services/impl/CitesphereManager.java b/vspace/src/main/java/edu/asu/diging/vspace/core/services/impl/CitesphereManager.java index e24f00c35..9c90a3854 100644 --- a/vspace/src/main/java/edu/asu/diging/vspace/core/services/impl/CitesphereManager.java +++ b/vspace/src/main/java/edu/asu/diging/vspace/core/services/impl/CitesphereManager.java @@ -1,6 +1,10 @@ package edu.asu.diging.vspace.core.services.impl; + +//import com.citesphere.api.CitesphereService; +//import com.citesphere.api.auth.CitesphereAuthToken; import com.fasterxml.jackson.databind.ObjectMapper; + import edu.asu.diging.vspace.core.services.CitesphereAuthToken; import edu.asu.diging.vspace.core.services.ICitesphereManager; @@ -9,310 +13,215 @@ import java.util.Base64; import java.util.HashMap; import java.util.Map; -import java.util.logging.Logger; -import java.util.logging.Level; - import okhttp3.*; /** - * Implementation of ICitesphereManager for Citesphere API operations. - * - * This class handles authentication, HTTP requests, and data management - * for interactions with the Citesphere API service. - * - * @author ASU Digital Innovation Group - * @version 1.0 + * Implementation of CitesphereService for API operations */ public class CitesphereManager implements ICitesphereManager { - private static final Logger LOGGER = Logger.getLogger(CitesphereManager.class.getName()); - - private static final String OAUTH_AUTH_TYPE = "oauth"; - private static final String BASIC_AUTH_TYPE = "basic"; - private static final String AUTHORIZATION_HEADER = "Authorization"; - private static final String BEARER_PREFIX = "Bearer "; - private static final String BASIC_PREFIX = "Basic "; - private static final String API_VERSION_PATH = "/v1"; - - private final String apiBaseUrl; + private final String api; private final CitesphereAuthToken authTokenObject; - private final OkHttpClient httpClient; + private final OkHttpClient client; private final ObjectMapper objectMapper; /** - * Constructor for CitesphereManager. - * - * @param apiBaseUrl The base URL for the Citesphere API - * @param authTokenObject Authentication token object containing credentials - * @throws IllegalArgumentException if authentication parameters are invalid + * Constructor + * @param api API base URL + * @param authTokenObject Authentication token object + * @return */ - public CitesphereManager(String apiBaseUrl, CitesphereAuthToken authTokenObject) { - this.apiBaseUrl = apiBaseUrl; + public CitesphereManager(String api, CitesphereAuthToken authTokenObject) { + this.api = api; this.authTokenObject = authTokenObject; - this.httpClient = new OkHttpClient(); + this.client = new OkHttpClient(); this.objectMapper = new ObjectMapper(); - validateAuthenticationParameters(); - configureAuthenticationHeaders(); + validate(); + handleApiParams(); } + /** - * Validates the authentication token object parameters. - * - * @throws IllegalArgumentException if authentication parameters are missing or invalid + * Validate authentication token object */ - private void validateAuthenticationParameters() { + private void validate() { if (authTokenObject.getAuthType() == null) { - throw new IllegalArgumentException("Authentication type (authType) is required"); + throw new IllegalArgumentException("Missing authType attribute"); } if (authTokenObject.getAccessToken() == null) { if (authTokenObject.getUsername() == null || authTokenObject.getPassword() == null) { throw new IllegalArgumentException( - "Either access token or username/password combination is required"); + "Either username and password or access_token should be present"); } } - if (!OAUTH_AUTH_TYPE.equals(authTokenObject.getAuthType()) && - !BASIC_AUTH_TYPE.equals(authTokenObject.getAuthType())) { - throw new IllegalArgumentException( - "Authentication type must be either '" + OAUTH_AUTH_TYPE + "' or '" + BASIC_AUTH_TYPE + "'"); + if (!"oauth".equals(authTokenObject.getAuthType()) && + !"basic".equals(authTokenObject.getAuthType())) { + throw new IllegalArgumentException("authType should be either oauth or basic"); } } /** - * Configures authentication headers based on the authentication type. + * Handle API parameters and set headers */ - private void configureAuthenticationHeaders() { + private void handleApiParams() { Map headers = new HashMap<>(); - if (OAUTH_AUTH_TYPE.equals(authTokenObject.getAuthType())) { - headers.put(AUTHORIZATION_HEADER, BEARER_PREFIX + authTokenObject.getAccessToken()); - } else if (BASIC_AUTH_TYPE.equals(authTokenObject.getAuthType())) { - String credentials = authTokenObject.getUsername() + ":" + authTokenObject.getPassword(); - String encodedCredentials = Base64.getEncoder().encodeToString(credentials.getBytes()); - headers.put(AUTHORIZATION_HEADER, BASIC_PREFIX + encodedCredentials); + if ("oauth".equals(authTokenObject.getAuthType())) { + headers.put("Authorization", "Bearer " + authTokenObject.getAccessToken()); + } else if ("basic".equals(authTokenObject.getAuthType())) { + String authStr = authTokenObject.getUsername() + ":" + authTokenObject.getPassword(); + String authB64 = Base64.getEncoder().encodeToString(authStr.getBytes()); + headers.put("Authorization", "Basic " + authB64); } authTokenObject.setHeaders(headers); } /** - * Executes a GET request to the specified URL. - * - * @param url The target URL for the GET request - * @return Response data as a Map, or error information if the request fails + * Execute GET command + * @param url Request URL + * @return Response data as Map */ - private Map executeGetRequest(String url) { + private Map executeCommand(String url) { try { Request.Builder requestBuilder = new Request.Builder().url(url); - // Add authentication headers - addAuthenticationHeaders(requestBuilder); + // Add headers + if (authTokenObject.getHeaders() != null) { + for (Map.Entry header : authTokenObject.getHeaders().entrySet()) { + requestBuilder.addHeader(header.getKey(), header.getValue()); + } + } Request request = requestBuilder.build(); - try (Response response = httpClient.newCall(request).execute()) { - return parseJsonResponse(response); + try (Response response = client.newCall(request).execute()) { + if (response.body() != null) { + String responseBody = response.body().string(); + return objectMapper.readValue(responseBody, Map.class); + } } - } catch (IOException e) { - LOGGER.log(Level.SEVERE, "Error executing GET request to " + url, e); - return createErrorResponse("Network error: " + e.getMessage()); } catch (Exception e) { - LOGGER.log(Level.SEVERE, "Unexpected error during GET request to " + url, e); - return createErrorResponse("Unexpected error: " + e.getMessage()); - } - } - - /** - * Adds authentication headers to the request builder. - * - * @param requestBuilder The request builder to add headers to - */ - private void addAuthenticationHeaders(Request.Builder requestBuilder) { - if (authTokenObject.getHeaders() != null) { - authTokenObject.getHeaders().forEach(requestBuilder::addHeader); - } - } - - /** - * Parses JSON response from HTTP response. - * - * @param response The HTTP response to parse - * @return Parsed JSON as Map - * @throws IOException if parsing fails - */ - @SuppressWarnings("unchecked") - private Map parseJsonResponse(Response response) throws IOException { - if (response.body() != null) { - String responseBody = response.body().string(); - if (!responseBody.isEmpty()) { - return objectMapper.readValue(responseBody, Map.class); - } + Map errorMap = new HashMap<>(); + errorMap.put("error_message", e.getMessage()); + return errorMap; } + return new HashMap<>(); } /** - * Creates an error response map. - * - * @param errorMessage The error message to include - * @return Map containing error information - */ - private Map createErrorResponse(String errorMessage) { - Map errorResponse = new HashMap<>(); - errorResponse.put("error", true); - errorResponse.put("error_message", errorMessage); - return errorResponse; - } - - /** - * Executes a POST request with optional file upload. - * - * @param url The target URL for the POST request - * @param data Form data to include in the request - * @param filePath Path to file for upload (optional) - * @return HTTP Response object or error message + * Execute POST request + * @param url Request URL + * @param data Request data + * @param filePath File path for upload + * @return Response object */ private Object executePostRequest(String url, Map data, String filePath) { try { - MultipartBody.Builder formBuilder = new MultipartBody.Builder() + MultipartBody.Builder builder = new MultipartBody.Builder() .setType(MultipartBody.FORM); - // Add form data parameters - addFormDataParameters(formBuilder, data); + // Add data parameters + if (data != null) { + for (Map.Entry entry : data.entrySet()) { + builder.addFormDataPart(entry.getKey(), entry.getValue().toString()); + } + } - // Add file if specified - addFileToForm(formBuilder, filePath); + // Add file if provided + if (filePath != null) { + File file = new File(filePath); + if (file.exists()) { + RequestBody fileBody = RequestBody.create(file, MediaType.parse("application/pdf")); + builder.addFormDataPart("files", file.getName(), fileBody); + } + } + + RequestBody requestBody = builder.build(); - RequestBody requestBody = formBuilder.build(); Request.Builder requestBuilder = new Request.Builder() .url(url) .post(requestBody); - addAuthenticationHeaders(requestBuilder); + // Add headers + if (authTokenObject.getHeaders() != null) { + for (Map.Entry header : authTokenObject.getHeaders().entrySet()) { + requestBuilder.addHeader(header.getKey(), header.getValue()); + } + } + Request request = requestBuilder.build(); - try (Response response = httpClient.newCall(request).execute()) { - LOGGER.info("POST request completed with response code: " + response.code()); + try (Response response = client.newCall(request).execute()) { + System.out.println("Response code: " + response.code()); return response; } - } catch (IOException e) { - LOGGER.log(Level.SEVERE, "IO error during POST request to " + url, e); - return "IO error during file upload: " + e.getMessage(); } catch (Exception e) { - LOGGER.log(Level.SEVERE, "Unexpected error during POST request to " + url, e); - return "Unexpected error during request: " + e.getMessage(); + System.err.println("[ERROR] -------- Error during API request with " + filePath + ": " + e.getMessage()); + return "Error loading/reading file"; } } - /** - * Adds form data parameters to the multipart form builder. - * - * @param formBuilder The form builder to add parameters to - * @param data The data parameters to add - */ - private void addFormDataParameters(MultipartBody.Builder formBuilder, Map data) { - if (data != null) { - data.forEach((key, value) -> - formBuilder.addFormDataPart(key, String.valueOf(value)) - ); - } - } - - /** - * Adds a file to the multipart form if the file path is provided and valid. - * - * @param formBuilder The form builder to add the file to - * @param filePath The path to the file to upload - */ - private void addFileToForm(MultipartBody.Builder formBuilder, String filePath) { - if (filePath != null && !filePath.trim().isEmpty()) { - File file = new File(filePath); - if (file.exists() && file.isFile()) { - RequestBody fileBody = RequestBody.create(file, MediaType.parse("application/pdf")); - formBuilder.addFormDataPart("files", file.getName(), fileBody); - } else { - LOGGER.warning("File not found or invalid: " + filePath); - } - } - } - - // API endpoint methods - @Override public Map getUser() { - return executeGetRequest(apiBaseUrl + API_VERSION_PATH + "/user"); + String url = api + "/v1/user"; + return executeCommand(url); } @Override public Map checkTest() { - return executeGetRequest(apiBaseUrl + API_VERSION_PATH + "/test"); + String url = api + "/v1/test"; + return executeCommand(url); } @Override public Map checkAccess(String documentId) { - if (documentId == null || documentId.trim().isEmpty()) { - return createErrorResponse("Document ID is required"); - } - return executeGetRequest(apiBaseUrl + "/files/giles/" + documentId + "/access/check"); + String url = api + "/files/giles/" + documentId + "/access/check"; + return executeCommand(url); } @Override public Map getDataByEndpoint(String endpoint) { - if (endpoint == null || endpoint.trim().isEmpty()) { - return createErrorResponse("Endpoint is required"); - } - return executeGetRequest(apiBaseUrl + API_VERSION_PATH + endpoint); + String url = api + "/v1" + endpoint; + return executeCommand(url); } @Override public Map getGroups() { - return executeGetRequest(apiBaseUrl + API_VERSION_PATH + "/groups"); + String url = api + "/v1/groups"; + return executeCommand(url); } @Override public Map getGroupInfo(String groupId) { - if (groupId == null || groupId.trim().isEmpty()) { - return createErrorResponse("Group ID is required"); - } - return executeGetRequest(apiBaseUrl + API_VERSION_PATH + "/groups/" + groupId); + String url = api + "/v1/groups/" + groupId; + return executeCommand(url); } @Override public Map getGroupItems(String zoteroGroupId) { - if (zoteroGroupId == null || zoteroGroupId.trim().isEmpty()) { - return createErrorResponse("Zotero Group ID is required"); - } - return executeGetRequest(apiBaseUrl + API_VERSION_PATH + "/groups/" + zoteroGroupId + "/items"); + String url = api + "/v1/groups/" + zoteroGroupId + "/items"; + return executeCommand(url); } @Override public Map getCollections(String zoteroGroupId) { - if (zoteroGroupId == null || zoteroGroupId.trim().isEmpty()) { - return createErrorResponse("Zotero Group ID is required"); - } - return executeGetRequest(apiBaseUrl + API_VERSION_PATH + "/groups/" + zoteroGroupId + "/collections"); + String url = api + "/v1/groups/" + zoteroGroupId + "/collections"; + return executeCommand(url); } @Override public Map getCollectionItems(String zoteroGroupId, String collectionId, int pageNumber) { - if (zoteroGroupId == null || zoteroGroupId.trim().isEmpty()) { - return createErrorResponse("Zotero Group ID is required"); - } - if (collectionId == null || collectionId.trim().isEmpty()) { - return createErrorResponse("Collection ID is required"); - } - - String url = apiBaseUrl + API_VERSION_PATH + "/groups/" + zoteroGroupId + - "/collections/" + collectionId + "/items"; - + String url = api + "/v1/groups/" + zoteroGroupId + "/collections/" + collectionId + "/items"; if (pageNumber > 0) { - url += "?page=" + pageNumber; + url += "?&page=" + pageNumber; } - - return executeGetRequest(url); + return executeCommand(url); } @Override @@ -322,32 +231,19 @@ public Map getCollectionItems(String zoteroGroupId, String colle @Override public Map getItemInfo(String zoteroGroupId, String itemId) { - if (zoteroGroupId == null || zoteroGroupId.trim().isEmpty()) { - return createErrorResponse("Zotero Group ID is required"); - } - if (itemId == null || itemId.trim().isEmpty()) { - return createErrorResponse("Item ID is required"); - } - return executeGetRequest(apiBaseUrl + API_VERSION_PATH + "/groups/" + zoteroGroupId + "/items/" + itemId); + String url = api + "/v1/groups/" + zoteroGroupId + "/items/" + itemId; + return executeCommand(url); } @Override public Map getCollectionsByCollectionId(String zoteroGroupId, String collectionId) { - if (zoteroGroupId == null || zoteroGroupId.trim().isEmpty()) { - return createErrorResponse("Zotero Group ID is required"); - } - if (collectionId == null || collectionId.trim().isEmpty()) { - return createErrorResponse("Collection ID is required"); - } - return executeGetRequest(apiBaseUrl + "/groups/" + zoteroGroupId + "/collections/" + collectionId + "/collections"); + String url = api + "/groups/" + zoteroGroupId + "/collections/" + collectionId + "/collections"; + return executeCommand(url); } @Override public Object addItem(String groupId, Map data, String filePath) { - if (groupId == null || groupId.trim().isEmpty()) { - return "Group ID is required"; - } - String url = apiBaseUrl + API_VERSION_PATH + "/groups/" + groupId + "/items/create"; + String url = api + "/v1/groups/" + groupId + "/items/create"; return executePostRequest(url, data, filePath); } -} \ No newline at end of file +} From 5e26a162e714e250eedcf180fdc064cfad930e13 Mon Sep 17 00:00:00 2001 From: Girik1105 Date: Mon, 8 Sep 2025 11:53:02 -0700 Subject: [PATCH 10/31] [VSPC-189] created citesphere controller to handle calls to citesphere --- vspace/pom.xml | 7 + .../web/staff/CitesphereController.java | 254 ++++++++++++++++++ vspace/src/main/resources/app.properties | 5 + vspace/src/main/resources/config.properties | 7 +- 4 files changed, 272 insertions(+), 1 deletion(-) create mode 100644 vspace/src/main/java/edu/asu/diging/vspace/web/staff/CitesphereController.java diff --git a/vspace/pom.xml b/vspace/pom.xml index c453493ef..844afaf9a 100644 --- a/vspace/pom.xml +++ b/vspace/pom.xml @@ -219,6 +219,13 @@ 0.15.0 + + + com.squareup.okhttp3 + okhttp + 4.9.3 + + org.thymeleaf diff --git a/vspace/src/main/java/edu/asu/diging/vspace/web/staff/CitesphereController.java b/vspace/src/main/java/edu/asu/diging/vspace/web/staff/CitesphereController.java new file mode 100644 index 000000000..999260800 --- /dev/null +++ b/vspace/src/main/java/edu/asu/diging/vspace/web/staff/CitesphereController.java @@ -0,0 +1,254 @@ +package edu.asu.diging.vspace.web.staff; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.stereotype.Controller; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestMethod; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.ResponseBody; + +import edu.asu.diging.vspace.core.model.IReference; +import edu.asu.diging.vspace.core.services.CitesphereAuthToken; +import edu.asu.diging.vspace.core.services.ICitesphereManager; +import edu.asu.diging.vspace.core.services.IReferenceManager; +import edu.asu.diging.vspace.core.services.impl.CitesphereManager; + +@Controller +public class CitesphereController { + + private final Logger logger = LoggerFactory.getLogger(getClass()); + + @Value("${citesphere_api_url:https://citesphere.org/api}") + private String citesphereApiUrl; + + @Value("${citesphere_username:}") + private String citesphereUsername; + + @Value("${citesphere_password:}") + private String citespherePassword; + + @Autowired + private IReferenceManager referenceManager; + + /** + * Get user groups from Citesphere + */ + @RequestMapping(value = "/staff/citesphere/groups", method = RequestMethod.GET) + @ResponseBody + public ResponseEntity> getGroups() { + try { + ICitesphereManager citesphereManager = createCitesphereManager(); + Map groups = citesphereManager.getGroups(); + return ResponseEntity.ok(groups); + } catch (Exception e) { + logger.error("Error fetching groups from Citesphere", e); + Map error = new HashMap<>(); + error.put("error", "Failed to fetch groups: " + e.getMessage()); + return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(error); + } + } + + /** + * Get collections for a specific group + */ + @RequestMapping(value = "/staff/citesphere/groups/{groupId}/collections", method = RequestMethod.GET) + @ResponseBody + public ResponseEntity> getCollections(@PathVariable String groupId) { + try { + ICitesphereManager citesphereManager = createCitesphereManager(); + Map collections = citesphereManager.getCollections(groupId); + return ResponseEntity.ok(collections); + } catch (Exception e) { + logger.error("Error fetching collections from Citesphere for group: " + groupId, e); + Map error = new HashMap<>(); + error.put("error", "Failed to fetch collections: " + e.getMessage()); + return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(error); + } + } + + /** + * Get items for a specific collection + */ + @RequestMapping(value = "/staff/citesphere/groups/{groupId}/collections/{collectionId}/items", method = RequestMethod.GET) + @ResponseBody + public ResponseEntity> getCollectionItems( + @PathVariable String groupId, + @PathVariable String collectionId, + @RequestParam(value = "page", defaultValue = "0") int page) { + try { + ICitesphereManager citesphereManager = createCitesphereManager(); + Map items = citesphereManager.getCollectionItems(groupId, collectionId, page); + return ResponseEntity.ok(items); + } catch (Exception e) { + logger.error("Error fetching collection items from Citesphere", e); + Map error = new HashMap<>(); + error.put("error", "Failed to fetch collection items: " + e.getMessage()); + return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(error); + } + } + + /** + * Get all items for a specific group + */ + @RequestMapping(value = "/staff/citesphere/groups/{groupId}/items", method = RequestMethod.GET) + @ResponseBody + public ResponseEntity> getGroupItems(@PathVariable String groupId) { + try { + ICitesphereManager citesphereManager = createCitesphereManager(); + Map items = citesphereManager.getGroupItems(groupId); + return ResponseEntity.ok(items); + } catch (Exception e) { + logger.error("Error fetching group items from Citesphere for group: " + groupId, e); + Map error = new HashMap<>(); + error.put("error", "Failed to fetch group items: " + e.getMessage()); + return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(error); + } + } + + /** + * Import selected references from Citesphere to bibliography + */ + @RequestMapping(value = "/staff/module/{moduleId}/slide/{slideId}/bibliography/{biblioId}/citesphere/import", method = RequestMethod.POST) + @ResponseBody + public ResponseEntity> importCitesphereReferences( + @PathVariable String moduleId, + @PathVariable String slideId, + @PathVariable String biblioId, + @RequestBody List> selectedReferences) { + + try { + List createdReferences = new ArrayList<>(); + + for (Map refData : selectedReferences) { + String title = extractField(refData, "title"); + String author = extractCreators(refData); + String year = extractYear(refData); + String journal = extractField(refData, "publicationTitle"); + String url = extractField(refData, "url"); + String volume = extractField(refData, "volume"); + String issue = extractField(refData, "issue"); + String pages = extractField(refData, "pages"); + String editors = extractField(refData, "editor"); + String type = extractField(refData, "itemType"); + String note = extractField(refData, "note"); + + IReference reference = referenceManager.createReference( + biblioId, title, author, year, journal, url, volume, issue, pages, editors, type, note + ); + + createdReferences.add(reference); + logger.info("Created reference: {}", title); + } + + Map response = new HashMap<>(); + response.put("success", true); + response.put("imported_count", createdReferences.size()); + response.put("references", createdReferences); + + return ResponseEntity.ok(response); + + } catch (Exception e) { + logger.error("Error importing references from Citesphere", e); + Map error = new HashMap<>(); + error.put("error", "Failed to import references: " + e.getMessage()); + return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(error); + } + } + + private ICitesphereManager createCitesphereManager() { + CitesphereAuthToken authToken = new CitesphereAuthToken(citesphereUsername, citespherePassword); + return new CitesphereManager(citesphereApiUrl, authToken); + } + + @SuppressWarnings("unchecked") + private String extractField(Map refData, String fieldName) { + try { + Map data = (Map) refData.get("data"); + if (data != null && data.containsKey(fieldName)) { + Object value = data.get(fieldName); + return value != null ? value.toString() : ""; + } + } catch (Exception e) { + logger.warn("Error extracting field {}: {}", fieldName, e.getMessage()); + } + return ""; + } + + /** + * Extract creators (authors) from Citesphere reference data + */ + @SuppressWarnings("unchecked") + private String extractCreators(Map refData) { + try { + Map data = (Map) refData.get("data"); + if (data != null && data.containsKey("creators")) { + List> creators = (List>) data.get("creators"); + StringBuilder authors = new StringBuilder(); + + for (Map creator : creators) { + if (authors.length() > 0) { + authors.append("; "); + } + String firstName = creator.getOrDefault("firstName", "").toString(); + String lastName = creator.getOrDefault("lastName", "").toString(); + + if (!lastName.isEmpty()) { + authors.append(lastName); + if (!firstName.isEmpty()) { + authors.append(", ").append(firstName); + } + } else if (!firstName.isEmpty()) { + authors.append(firstName); + } + } + + return authors.toString(); + } + } catch (Exception e) { + logger.warn("Error extracting creators: {}", e.getMessage()); + } + return ""; + } + + /** + * Extract year from Citesphere reference data + */ + @SuppressWarnings("unchecked") + private String extractYear(Map refData) { + try { + Map data = (Map) refData.get("data"); + if (data != null) { + String date = extractField(refData, "date"); + if (!date.isEmpty()) { + String[] parts = date.split("-"); + if (parts.length > 0 && parts[0].matches("\\d{4}")) { + return parts[0]; + } + } + + String accessDate = extractField(refData, "accessDate"); + if (!accessDate.isEmpty()) { + String[] parts = accessDate.split("-"); + if (parts.length > 0 && parts[0].matches("\\d{4}")) { + return parts[0]; + } + } + } + } catch (Exception e) { + logger.warn("Error extracting year: {}", e.getMessage()); + } + return ""; + } +} diff --git a/vspace/src/main/resources/app.properties b/vspace/src/main/resources/app.properties index bebd58784..12e88020f 100644 --- a/vspace/src/main/resources/app.properties +++ b/vspace/src/main/resources/app.properties @@ -15,3 +15,8 @@ file_uploads_directory=${files.directory.path} hibernate_show_sql=${hibernate.show_sql} admin_username=${admin.username} + +# Citesphere integration +citesphere.api.url=${citesphere_api_url} +citesphere.username=${citesphere_username} +citesphere.password=${citesphere_password} diff --git a/vspace/src/main/resources/config.properties b/vspace/src/main/resources/config.properties index adf448ef1..efc0a4077 100644 --- a/vspace/src/main/resources/config.properties +++ b/vspace/src/main/resources/config.properties @@ -13,4 +13,9 @@ image_category_SPACE_BACKGROUND_IMAGE=Space Background Image image_category_SLIDE_IMAGE=Slide Image image_category_LINK_IMAGE=Link Image buildNum=${buildNumber} -baseUrl=${app.url} \ No newline at end of file +baseUrl=${app.url} + +# Citesphere API Configuration +citesphere_api_url=${citesphere.api.url:https://citesphere.org/api} +citesphere_username=${citesphere.username:} +citesphere_password=${citesphere.password:} \ No newline at end of file From e8fca0ab97234674e2bf6b9697715c125e652376 Mon Sep 17 00:00:00 2001 From: Girik1105 Date: Wed, 10 Sep 2025 11:55:04 -0700 Subject: [PATCH 11/31] [VSPC-189] Added auoth flow to connect with citesphere --- .../web/staff/CitesphereController.java | 169 ++++++++++++++++-- 1 file changed, 154 insertions(+), 15 deletions(-) diff --git a/vspace/src/main/java/edu/asu/diging/vspace/web/staff/CitesphereController.java b/vspace/src/main/java/edu/asu/diging/vspace/web/staff/CitesphereController.java index 999260800..125c8f393 100644 --- a/vspace/src/main/java/edu/asu/diging/vspace/web/staff/CitesphereController.java +++ b/vspace/src/main/java/edu/asu/diging/vspace/web/staff/CitesphereController.java @@ -18,7 +18,13 @@ import org.springframework.web.bind.annotation.RequestMethod; import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.ResponseBody; +import org.springframework.ui.Model; +import org.springframework.web.servlet.mvc.support.RedirectAttributes; +import javax.servlet.http.HttpSession; +import java.io.UnsupportedEncodingException; +import com.fasterxml.jackson.databind.ObjectMapper; +import okhttp3.*; import edu.asu.diging.vspace.core.model.IReference; import edu.asu.diging.vspace.core.services.CitesphereAuthToken; import edu.asu.diging.vspace.core.services.ICitesphereManager; @@ -33,23 +39,100 @@ public class CitesphereController { @Value("${citesphere_api_url:https://citesphere.org/api}") private String citesphereApiUrl; - @Value("${citesphere_username:}") - private String citesphereUsername; + @Value("${citesphere_client_id:}") + private String citesphereClientId; - @Value("${citesphere_password:}") - private String citespherePassword; + @Value("${citesphere_client_secret:}") + private String citesphereClientSecret; + + @Value("${app_base_url:}") + private String appBaseUrl; @Autowired private IReferenceManager referenceManager; + + /** + * Initiate OAuth authorization with Citesphere + */ + @RequestMapping(value = "/staff/citesphere/oauth/authorize", method = RequestMethod.GET) + public String initiateOAuth(HttpSession session, RedirectAttributes redirectAttributes) { + if (citesphereClientId == null || citesphereClientId.isEmpty()) { + redirectAttributes.addFlashAttribute("error", "Citesphere OAuth is not configured. Please contact your administrator."); + return "redirect:/staff/dashboard"; + } + + try { + // Generate state parameter for security + String state = java.util.UUID.randomUUID().toString(); + session.setAttribute("citesphere_oauth_state", state); + + // Build authorization URL + String baseUrl = citesphereApiUrl.replace("/api", ""); + String redirectUri = getCurrentBaseUrl() + "/staff/citesphere/oauth/callback"; + + String authUrl = baseUrl + "/oauth/authorize" + + "?response_type=code" + + "&client_id=" + java.net.URLEncoder.encode(citesphereClientId, "UTF-8") + + "&state=" + java.net.URLEncoder.encode(state, "UTF-8") + + "&redirect_uri=" + java.net.URLEncoder.encode(redirectUri, "UTF-8"); + + return "redirect:" + authUrl; + } catch (UnsupportedEncodingException e) { + logger.error("Error encoding OAuth parameters", e); + redirectAttributes.addFlashAttribute("error", "Error initiating OAuth flow."); + return "redirect:/staff/dashboard"; + } + } + + /** + * Handle OAuth callback from Citesphere + */ + @RequestMapping(value = "/staff/citesphere/oauth/callback", method = RequestMethod.GET) + public String handleOAuthCallback( + @RequestParam String code, + @RequestParam String state, + HttpSession session, + RedirectAttributes redirectAttributes) { + + try { + // Verify state parameter + String sessionState = (String) session.getAttribute("citesphere_oauth_state"); + if (sessionState == null || !sessionState.equals(state)) { + redirectAttributes.addFlashAttribute("error", "Invalid OAuth state. Please try again."); + return "redirect:/staff/dashboard"; + } + + // Exchange code for access token + String accessToken = exchangeCodeForToken(code); + if (accessToken != null) { + // Store token in session + session.setAttribute("citesphere_access_token", accessToken); + redirectAttributes.addFlashAttribute("success", "Successfully connected to Citesphere!"); + } else { + redirectAttributes.addFlashAttribute("error", "Failed to obtain access token from Citesphere."); + } + + } catch (Exception e) { + logger.error("Error handling OAuth callback", e); + redirectAttributes.addFlashAttribute("error", "OAuth authentication failed: " + e.getMessage()); + } finally { + // Clean up session + session.removeAttribute("citesphere_oauth_state"); + } + + return "redirect:/staff/dashboard"; + } + + /** * Get user groups from Citesphere */ @RequestMapping(value = "/staff/citesphere/groups", method = RequestMethod.GET) @ResponseBody - public ResponseEntity> getGroups() { + public ResponseEntity> getGroups(HttpSession session) { try { - ICitesphereManager citesphereManager = createCitesphereManager(); + ICitesphereManager citesphereManager = createCitesphereManager(session); Map groups = citesphereManager.getGroups(); return ResponseEntity.ok(groups); } catch (Exception e) { @@ -65,9 +148,9 @@ public ResponseEntity> getGroups() { */ @RequestMapping(value = "/staff/citesphere/groups/{groupId}/collections", method = RequestMethod.GET) @ResponseBody - public ResponseEntity> getCollections(@PathVariable String groupId) { + public ResponseEntity> getCollections(@PathVariable String groupId, HttpSession session) { try { - ICitesphereManager citesphereManager = createCitesphereManager(); + ICitesphereManager citesphereManager = createCitesphereManager(session); Map collections = citesphereManager.getCollections(groupId); return ResponseEntity.ok(collections); } catch (Exception e) { @@ -86,9 +169,10 @@ public ResponseEntity> getCollections(@PathVariable String g public ResponseEntity> getCollectionItems( @PathVariable String groupId, @PathVariable String collectionId, - @RequestParam(value = "page", defaultValue = "0") int page) { + @RequestParam(value = "page", defaultValue = "0") int page, + HttpSession session) { try { - ICitesphereManager citesphereManager = createCitesphereManager(); + ICitesphereManager citesphereManager = createCitesphereManager(session); Map items = citesphereManager.getCollectionItems(groupId, collectionId, page); return ResponseEntity.ok(items); } catch (Exception e) { @@ -104,9 +188,9 @@ public ResponseEntity> getCollectionItems( */ @RequestMapping(value = "/staff/citesphere/groups/{groupId}/items", method = RequestMethod.GET) @ResponseBody - public ResponseEntity> getGroupItems(@PathVariable String groupId) { + public ResponseEntity> getGroupItems(@PathVariable String groupId, HttpSession session) { try { - ICitesphereManager citesphereManager = createCitesphereManager(); + ICitesphereManager citesphereManager = createCitesphereManager(session); Map items = citesphereManager.getGroupItems(groupId); return ResponseEntity.ok(items); } catch (Exception e) { @@ -167,9 +251,64 @@ public ResponseEntity> importCitesphereReferences( } } - private ICitesphereManager createCitesphereManager() { - CitesphereAuthToken authToken = new CitesphereAuthToken(citesphereUsername, citespherePassword); - return new CitesphereManager(citesphereApiUrl, authToken); + private ICitesphereManager createCitesphereManager(HttpSession session) { + // Check for OAuth token + String accessToken = (String) session.getAttribute("citesphere_access_token"); + + if (accessToken != null) { + // Use OAuth token + CitesphereAuthToken authToken = new CitesphereAuthToken(accessToken); + return new CitesphereManager(citesphereApiUrl, authToken); + } else { + // No authentication available + throw new IllegalStateException("No Citesphere access token available. Please authenticate first."); + } + } + + /** + * Exchange authorization code for access token + */ + private String exchangeCodeForToken(String code) { + try { + OkHttpClient client = new OkHttpClient(); + + RequestBody formBody = new FormBody.Builder() + .add("grant_type", "authorization_code") + .add("code", code) + .add("client_id", citesphereClientId) + .add("client_secret", citesphereClientSecret) + .add("redirect_uri", getCurrentBaseUrl() + "/staff/citesphere/oauth/callback") + .build(); + + Request request = new Request.Builder() + .url(citesphereApiUrl.replace("/api", "") + "/oauth/token") + .post(formBody) + .build(); + + try (Response response = client.newCall(request).execute()) { + String responseBody = response.body() != null ? response.body().string() : ""; + logger.debug("Token exchange response: {} - {}", response.code(), responseBody); + + if (response.isSuccessful() && !responseBody.isEmpty()) { + ObjectMapper mapper = new ObjectMapper(); + @SuppressWarnings("unchecked") + Map tokenResponse = mapper.readValue(responseBody, Map.class); + return (String) tokenResponse.get("access_token"); + } else { + logger.error("Token exchange failed: {} - {}", response.code(), responseBody); + } + } + } catch (Exception e) { + logger.error("Error exchanging code for token", e); + } + return null; + } + + /** + * Get current base URL for redirect URI + */ + private String getCurrentBaseUrl() { + return appBaseUrl != null && !appBaseUrl.isEmpty() ? appBaseUrl : "http://localhost:8080"; } @SuppressWarnings("unchecked") From 6194bba7f09993e13dcdf19f67fb8eb382dc173e Mon Sep 17 00:00:00 2001 From: Girik1105 Date: Thu, 11 Sep 2025 11:58:04 -0700 Subject: [PATCH 12/31] [VSPC-189] Added debug print, fixed endpoints, added implmentation in frontend - need to fix api errors --- .../web/staff/CitesphereController.java | 103 ++++- .../views/staff/modules/slides/slide.html | 389 ++++++++++++++++++ 2 files changed, 476 insertions(+), 16 deletions(-) diff --git a/vspace/src/main/java/edu/asu/diging/vspace/web/staff/CitesphereController.java b/vspace/src/main/java/edu/asu/diging/vspace/web/staff/CitesphereController.java index 125c8f393..5e2417db5 100644 --- a/vspace/src/main/java/edu/asu/diging/vspace/web/staff/CitesphereController.java +++ b/vspace/src/main/java/edu/asu/diging/vspace/web/staff/CitesphereController.java @@ -36,16 +36,16 @@ public class CitesphereController { private final Logger logger = LoggerFactory.getLogger(getClass()); - @Value("${citesphere_api_url:https://citesphere.org/api}") + @Value("${citesphere.api.url:}") private String citesphereApiUrl; - @Value("${citesphere_client_id:}") + @Value("${citesphere.client.id:}") private String citesphereClientId; - @Value("${citesphere_client_secret:}") + @Value("${citesphere.client.secret:}") private String citesphereClientSecret; - @Value("${app_base_url:}") + @Value("${app.base.url:http://localhost:8080}") private String appBaseUrl; @Autowired @@ -55,9 +55,15 @@ public class CitesphereController { /** * Initiate OAuth authorization with Citesphere */ - @RequestMapping(value = "/staff/citesphere/oauth/authorize", method = RequestMethod.GET) + @RequestMapping(value = "/api/oauth/authorize", method = RequestMethod.GET) public String initiateOAuth(HttpSession session, RedirectAttributes redirectAttributes) { + logger.info("[DEBUG] Initiating OAuth authorization with Citesphere"); + logger.info("[DEBUG] Citesphere API URL: {}", citesphereApiUrl); + logger.info("[DEBUG] Client ID configured: {}", (citesphereClientId != null && !citesphereClientId.isEmpty())); + logger.info("[DEBUG] Client Secret configured: {}", (citesphereClientSecret != null && !citesphereClientSecret.isEmpty())); + if (citesphereClientId == null || citesphereClientId.isEmpty()) { + logger.error("[DEBUG] OAuth not configured - missing client ID"); redirectAttributes.addFlashAttribute("error", "Citesphere OAuth is not configured. Please contact your administrator."); return "redirect:/staff/dashboard"; } @@ -71,12 +77,17 @@ public String initiateOAuth(HttpSession session, RedirectAttributes redirectAttr String baseUrl = citesphereApiUrl.replace("/api", ""); String redirectUri = getCurrentBaseUrl() + "/staff/citesphere/oauth/callback"; + logger.info("[DEBUG] Building OAuth URL - Base URL: {}", baseUrl); + logger.info("[DEBUG] Redirect URI: {}", redirectUri); + logger.info("[DEBUG] OAuth State: {}", state); + String authUrl = baseUrl + "/oauth/authorize" + "?response_type=code" + "&client_id=" + java.net.URLEncoder.encode(citesphereClientId, "UTF-8") + "&state=" + java.net.URLEncoder.encode(state, "UTF-8") + "&redirect_uri=" + java.net.URLEncoder.encode(redirectUri, "UTF-8"); + logger.info("[DEBUG] Final OAuth URL: {}", authUrl); return "redirect:" + authUrl; } catch (UnsupportedEncodingException e) { logger.error("Error encoding OAuth parameters", e); @@ -95,21 +106,35 @@ public String handleOAuthCallback( HttpSession session, RedirectAttributes redirectAttributes) { + logger.info("[DEBUG] OAuth callback received"); + logger.info("[DEBUG] Authorization code: {}", code != null ? "[PRESENT]" : "[MISSING]"); + logger.info("[DEBUG] State parameter: {}", state); + try { // Verify state parameter String sessionState = (String) session.getAttribute("citesphere_oauth_state"); + logger.info("[DEBUG] Session state: {}", sessionState); + logger.info("[DEBUG] Received state: {}", state); + logger.info("[DEBUG] State match: {}", sessionState != null && sessionState.equals(state)); + if (sessionState == null || !sessionState.equals(state)) { + logger.error("[DEBUG] OAuth state validation failed"); redirectAttributes.addFlashAttribute("error", "Invalid OAuth state. Please try again."); return "redirect:/staff/dashboard"; } // Exchange code for access token + logger.info("[DEBUG] Exchanging authorization code for access token"); String accessToken = exchangeCodeForToken(code); + logger.info("[DEBUG] Access token received: {}", accessToken != null ? "[SUCCESS]" : "[FAILED]"); + if (accessToken != null) { // Store token in session session.setAttribute("citesphere_access_token", accessToken); + logger.info("[DEBUG] Access token stored in session"); redirectAttributes.addFlashAttribute("success", "Successfully connected to Citesphere!"); } else { + logger.error("[DEBUG] Failed to obtain access token"); redirectAttributes.addFlashAttribute("error", "Failed to obtain access token from Citesphere."); } @@ -128,12 +153,17 @@ public String handleOAuthCallback( /** * Get user groups from Citesphere */ - @RequestMapping(value = "/staff/citesphere/groups", method = RequestMethod.GET) + @RequestMapping(value = "/api/v1/citesphere/groups", method = RequestMethod.GET) @ResponseBody public ResponseEntity> getGroups(HttpSession session) { + logger.info("[DEBUG] API call: getGroups() - Fetching user groups from Citesphere"); + try { ICitesphereManager citesphereManager = createCitesphereManager(session); + logger.info("[DEBUG] CitesphereManager created successfully, making API call to get groups"); Map groups = citesphereManager.getGroups(); + logger.info("[DEBUG] Groups retrieved successfully: {} groups found", + groups != null && groups.containsKey("data") ? "[DATA_PRESENT]" : "[NO_DATA]"); return ResponseEntity.ok(groups); } catch (Exception e) { logger.error("Error fetching groups from Citesphere", e); @@ -146,12 +176,17 @@ public ResponseEntity> getGroups(HttpSession session) { /** * Get collections for a specific group */ - @RequestMapping(value = "/staff/citesphere/groups/{groupId}/collections", method = RequestMethod.GET) + @RequestMapping(value = "/api/v1/citesphere/groups/{groupId}/collections", method = RequestMethod.GET) @ResponseBody public ResponseEntity> getCollections(@PathVariable String groupId, HttpSession session) { + logger.info("[DEBUG] API call: getCollections() - Group ID: {}", groupId); + try { ICitesphereManager citesphereManager = createCitesphereManager(session); + logger.info("[DEBUG] CitesphereManager created, fetching collections for group: {}", groupId); Map collections = citesphereManager.getCollections(groupId); + logger.info("[DEBUG] Collections retrieved successfully for group {}: {} collections found", + groupId, collections != null && collections.containsKey("data") ? "[DATA_PRESENT]" : "[NO_DATA]"); return ResponseEntity.ok(collections); } catch (Exception e) { logger.error("Error fetching collections from Citesphere for group: " + groupId, e); @@ -164,16 +199,21 @@ public ResponseEntity> getCollections(@PathVariable String g /** * Get items for a specific collection */ - @RequestMapping(value = "/staff/citesphere/groups/{groupId}/collections/{collectionId}/items", method = RequestMethod.GET) + @RequestMapping(value = "/api/v1/groups/{groupId}/collections/{collectionId}/items", method = RequestMethod.GET) @ResponseBody public ResponseEntity> getCollectionItems( @PathVariable String groupId, @PathVariable String collectionId, @RequestParam(value = "page", defaultValue = "0") int page, HttpSession session) { + logger.info("[DEBUG] API call: getCollectionItems() - Group: {}, Collection: {}, Page: {}", groupId, collectionId, page); + try { ICitesphereManager citesphereManager = createCitesphereManager(session); + logger.info("[DEBUG] CitesphereManager created, fetching items for collection: {} in group: {}", collectionId, groupId); Map items = citesphereManager.getCollectionItems(groupId, collectionId, page); + logger.info("[DEBUG] Collection items retrieved successfully: {} items found", + items != null && items.containsKey("data") ? "[DATA_PRESENT]" : "[NO_DATA]"); return ResponseEntity.ok(items); } catch (Exception e) { logger.error("Error fetching collection items from Citesphere", e); @@ -186,12 +226,17 @@ public ResponseEntity> getCollectionItems( /** * Get all items for a specific group */ - @RequestMapping(value = "/staff/citesphere/groups/{groupId}/items", method = RequestMethod.GET) + @RequestMapping(value = "/api/v1/groups/{groupId}/items", method = RequestMethod.GET) @ResponseBody public ResponseEntity> getGroupItems(@PathVariable String groupId, HttpSession session) { + logger.info("[DEBUG] API call: getGroupItems() - Group ID: {}", groupId); + try { ICitesphereManager citesphereManager = createCitesphereManager(session); + logger.info("[DEBUG] CitesphereManager created, fetching all items for group: {}", groupId); Map items = citesphereManager.getGroupItems(groupId); + logger.info("[DEBUG] Group items retrieved successfully: {} items found", + items != null && items.containsKey("data") ? "[DATA_PRESENT]" : "[NO_DATA]"); return ResponseEntity.ok(items); } catch (Exception e) { logger.error("Error fetching group items from Citesphere for group: " + groupId, e); @@ -204,7 +249,7 @@ public ResponseEntity> getGroupItems(@PathVariable String gr /** * Import selected references from Citesphere to bibliography */ - @RequestMapping(value = "/staff/module/{moduleId}/slide/{slideId}/bibliography/{biblioId}/citesphere/import", method = RequestMethod.POST) + @RequestMapping(value = "/api/v1/groups/{groupId}/collections/{collectionId}/items/", method = RequestMethod.POST) @ResponseBody public ResponseEntity> importCitesphereReferences( @PathVariable String moduleId, @@ -212,6 +257,9 @@ public ResponseEntity> importCitesphereReferences( @PathVariable String biblioId, @RequestBody List> selectedReferences) { + logger.info("[DEBUG] API call: importCitesphereReferences() - Module: {}, Slide: {}, Bibliography: {}", moduleId, slideId, biblioId); + logger.info("[DEBUG] Number of references to import: {}", selectedReferences != null ? selectedReferences.size() : 0); + try { List createdReferences = new ArrayList<>(); @@ -252,15 +300,22 @@ public ResponseEntity> importCitesphereReferences( } private ICitesphereManager createCitesphereManager(HttpSession session) { + logger.info("[DEBUG] Creating CitesphereManager"); + // Check for OAuth token String accessToken = (String) session.getAttribute("citesphere_access_token"); + logger.info("[DEBUG] Access token in session: {}", accessToken != null ? "[PRESENT]" : "[MISSING]"); + logger.info("[DEBUG] API URL: {}", citesphereApiUrl); if (accessToken != null) { // Use OAuth token + logger.info("[DEBUG] Creating CitesphereAuthToken with access token"); CitesphereAuthToken authToken = new CitesphereAuthToken(accessToken); + logger.info("[DEBUG] Creating CitesphereManager with API URL: {} and auth token", citesphereApiUrl); return new CitesphereManager(citesphereApiUrl, authToken); } else { // No authentication available + logger.error("[DEBUG] No access token available in session - authentication required"); throw new IllegalStateException("No Citesphere access token available. Please authenticate first."); } } @@ -269,33 +324,49 @@ private ICitesphereManager createCitesphereManager(HttpSession session) { * Exchange authorization code for access token */ private String exchangeCodeForToken(String code) { + logger.info("[DEBUG] Starting token exchange process"); + logger.info("[DEBUG] Authorization code: {}", code != null ? "[PRESENT]" : "[MISSING]"); + try { OkHttpClient client = new OkHttpClient(); + String redirectUri = getCurrentBaseUrl() + "/staff/citesphere/oauth/callback"; + String tokenUrl = citesphereApiUrl.replace("/api", "") + "/oauth/token"; + + logger.info("[DEBUG] Token exchange URL: {}", tokenUrl); + logger.info("[DEBUG] Redirect URI: {}", redirectUri); + logger.info("[DEBUG] Client ID: {}", citesphereClientId != null ? "[PRESENT]" : "[MISSING]"); + logger.info("[DEBUG] Client Secret: {}", citesphereClientSecret != null ? "[PRESENT]" : "[MISSING]"); - RequestBody formBody = new FormBody.Builder() + okhttp3.RequestBody formBody = new FormBody.Builder() .add("grant_type", "authorization_code") .add("code", code) .add("client_id", citesphereClientId) .add("client_secret", citesphereClientSecret) - .add("redirect_uri", getCurrentBaseUrl() + "/staff/citesphere/oauth/callback") + .add("redirect_uri", redirectUri) .build(); Request request = new Request.Builder() - .url(citesphereApiUrl.replace("/api", "") + "/oauth/token") + .url(tokenUrl) .post(formBody) .build(); + logger.info("[DEBUG] Making token exchange request to: {}", tokenUrl); + try (Response response = client.newCall(request).execute()) { String responseBody = response.body() != null ? response.body().string() : ""; - logger.debug("Token exchange response: {} - {}", response.code(), responseBody); + logger.info("[DEBUG] Token exchange response code: {}", response.code()); + logger.info("[DEBUG] Token exchange response body: {}", responseBody); if (response.isSuccessful() && !responseBody.isEmpty()) { + logger.info("[DEBUG] Token exchange successful, parsing response"); ObjectMapper mapper = new ObjectMapper(); @SuppressWarnings("unchecked") Map tokenResponse = mapper.readValue(responseBody, Map.class); - return (String) tokenResponse.get("access_token"); + String accessToken = (String) tokenResponse.get("access_token"); + logger.info("[DEBUG] Access token extracted: {}", accessToken != null ? "[SUCCESS]" : "[FAILED]"); + return accessToken; } else { - logger.error("Token exchange failed: {} - {}", response.code(), responseBody); + logger.error("[DEBUG] Token exchange failed - Response code: {}, Body: {}", response.code(), responseBody); } } } catch (Exception e) { diff --git a/vspace/src/main/webapp/WEB-INF/views/staff/modules/slides/slide.html b/vspace/src/main/webapp/WEB-INF/views/staff/modules/slides/slide.html index 5c7317dc8..72a83f2f8 100644 --- a/vspace/src/main/webapp/WEB-INF/views/staff/modules/slides/slide.html +++ b/vspace/src/main/webapp/WEB-INF/views/staff/modules/slides/slide.html @@ -2668,6 +2668,395 @@ $("#addVideoAlert").show(); } }); + + + var citesphereData = { + selectedReferences: [], + groups: [], + collections: {} + }; + + // Initialize Citesphere functionality when modal is shown + $('#addReferenceAlert').on('shown.bs.modal', function () { + // Show/hide appropriate tab content and buttons based on active tab + updateReferenceModalButtons(); + + // If Citesphere tab is active, load initial data + if ($('#citesphereRefTab').hasClass('active')) { + loadCitesphereGroups(); + } + }); + + // Tab switching handlers + $('a[data-toggle="tab"]').on('shown.bs.tab', function (e) { + updateReferenceModalButtons(); + + // Load Citesphere data when switching to Citesphere tab + if ($(e.target).attr('href') === '#citesphereRefTab') { + loadCitesphereGroups(); + } + }); + + function updateReferenceModalButtons() { + var isCitesphereTab = $('#citesphereRefTab').hasClass('active'); + + if (isCitesphereTab) { + $('#submitReference').hide(); + $('#submitCitesphereReferences').show(); + } else { + $('#submitReference').show(); + $('#submitCitesphereReferences').hide(); + } + } + + // Load user groups from Citesphere + function loadCitesphereGroups() { + var token = $('input[name="_csrf"]').attr('value'); + + $.ajax({ + url: '/staff/citesphere/groups', + type: 'GET', + headers: { + 'X-CSRF-Token': token + }, + success: function(data) { + displayCitesphereGroups(data); + citesphereData.groups = data; + }, + error: function(xhr, status, error) { + console.error('Error loading Citesphere groups:', error); + if (xhr.status === 500 && xhr.responseText && xhr.responseText.includes('access token')) { + // User needs to authenticate + $('#citesphereCollections').html( + '
' + + '
Authentication Required
' + + '

You need to authenticate with Citesphere to access your bibliography data.

' + + '' + + ' Connect to Citesphere' + + '' + + '
' + ); + } else { + $('#citesphereCollections').html('
Error loading groups. Please check your Citesphere credentials.
'); + } + } + }); + } + + // Display groups and collections + function displayCitesphereGroups(groupsData) { + var collectionsContainer = $('#citesphereCollections'); + collectionsContainer.empty(); + + if (!groupsData || !groupsData.data || !Array.isArray(groupsData.data)) { + collectionsContainer.html('
No groups found.
'); + return; + } + + groupsData.data.forEach(function(group) { + var groupElement = $('
' + + '
' + (group.name || 'Unnamed Group') + '
' + + 'Click to load collections' + + '
'); + + groupElement.click(function() { + loadGroupCollections(group.zoteroGroupId || group.id, group.name); + }); + + collectionsContainer.append(groupElement); + }); + } + + // Load collections for a specific group + function loadGroupCollections(groupId, groupName) { + var token = $('input[name="_csrf"]').attr('value'); + + $.ajax({ + url: '/staff/citesphere/groups/' + groupId + '/collections', + type: 'GET', + headers: { + 'X-CSRF-Token': token + }, + success: function(data) { + displayCollections(data, groupId, groupName); + loadGroupItems(groupId, groupName); // Also load individual items + }, + error: function(xhr, status, error) { + console.error('Error loading collections for group ' + groupId + ':', error); + } + }); + } + + // Display collections for a group + function displayCollections(collectionsData, groupId, groupName) { + var collectionsContainer = $('#citesphereCollections'); + collectionsContainer.empty(); + + // Add back button + var backButton = $(''); + backButton.click(function() { + loadCitesphereGroups(); + }); + collectionsContainer.append(backButton); + + var groupHeader = $('
' + groupName + ' - Collections
'); + collectionsContainer.append(groupHeader); + + if (!collectionsData || !collectionsData.data || !Array.isArray(collectionsData.data)) { + collectionsContainer.append('
No collections found in this group.
'); + return; + } + + collectionsData.data.forEach(function(collection) { + var collectionElement = $('
' + + '
' + (collection.data.name || 'Unnamed Collection') + '
' + + 'Click to load items' + + '
'); + + collectionElement.click(function() { + loadCollectionItems(groupId, collection.data.key, collection.data.name); + }); + + collectionsContainer.append(collectionElement); + }); + } + + // Load individual items for a group (not in collections) + function loadGroupItems(groupId, groupName) { + var token = $('input[name="_csrf"]').attr('value'); + + $.ajax({ + url: '/staff/citesphere/groups/' + groupId + '/items', + type: 'GET', + headers: { + 'X-CSRF-Token': token + }, + success: function(data) { + displayIndividualItems(data, 'Individual Items in ' + groupName); + }, + error: function(xhr, status, error) { + console.error('Error loading group items:', error); + } + }); + } + + // Load items from a specific collection + function loadCollectionItems(groupId, collectionId, collectionName) { + var token = $('input[name="_csrf"]').attr('value'); + + $.ajax({ + url: '/staff/citesphere/groups/' + groupId + '/collections/' + collectionId + '/items', + type: 'GET', + headers: { + 'X-CSRF-Token': token + }, + success: function(data) { + displayIndividualItems(data, 'Items in ' + collectionName); + }, + error: function(xhr, status, error) { + console.error('Error loading collection items:', error); + } + }); + } + + // Display individual references/items + function displayIndividualItems(itemsData, title) { + var referencesContainer = $('#citesphereReferences'); + referencesContainer.empty(); + + var titleElement = $('
' + title + '
'); + referencesContainer.append(titleElement); + + if (!itemsData || !itemsData.data || !Array.isArray(itemsData.data)) { + referencesContainer.append('
No items found.
'); + return; + } + + itemsData.data.forEach(function(item) { + var itemData = item.data || {}; + var title = itemData.title || 'Untitled'; + var author = extractAuthorsFromItem(itemData); + var year = extractYearFromItem(itemData); + var type = itemData.itemType || 'unknown'; + + var checkbox = $(''); + checkbox.data('item', item); + + var itemElement = $('
' + + '
' + + '
' + + '
' + + '
' + title + '
' + + '
' + author + (year ? ' (' + year + ')' : '') + '
' + + '
Type: ' + type + '
' + + '
' + + '
' + + '
'); + + itemElement.find('div').first().prepend(checkbox); + + // Handle selection + checkbox.change(function() { + if (this.checked) { + citesphereData.selectedReferences.push(item); + } else { + citesphereData.selectedReferences = citesphereData.selectedReferences.filter(function(ref) { + return ref.key !== item.key; + }); + } + updateSelectedCount(); + }); + + referencesContainer.append(itemElement); + }); + } + + // Helper function to extract authors from Citesphere item + function extractAuthorsFromItem(itemData) { + if (!itemData.creators || !Array.isArray(itemData.creators)) { + return 'Unknown Author'; + } + + var authors = itemData.creators.map(function(creator) { + var firstName = creator.firstName || ''; + var lastName = creator.lastName || ''; + + if (lastName && firstName) { + return lastName + ', ' + firstName; + } else if (lastName) { + return lastName; + } else if (firstName) { + return firstName; + } + return ''; + }).filter(Boolean); + + return authors.length > 0 ? authors.join('; ') : 'Unknown Author'; + } + + // Helper function to extract year from Citesphere item + function extractYearFromItem(itemData) { + var date = itemData.date || itemData.accessDate || ''; + if (date) { + var match = date.match(/(\d{4})/); + return match ? match[1] : ''; + } + return ''; + } + + // Update selected count display + function updateSelectedCount() { + var count = citesphereData.selectedReferences.length; + $('#selectedCountText').text(count + ' reference' + (count !== 1 ? 's' : '')); + } + + // Handle search functionality + $('#searchCitesphereBtn').click(function() { + var searchTerm = $('#citesphereSearch').val().trim(); + if (searchTerm) { + // For now, just show an informative message since Citesphere API doesn't have search endpoint yet + $('#citesphereReferences').html( + '
' + + '
Search Not Available
' + + '

Search functionality is not yet available in the Citesphere API. Please browse through your groups and collections above to find references.

' + + '
' + ); + } else { + // Reload current view + loadCitesphereGroups(); + } + }); + + // Allow search on Enter key + $('#citesphereSearch').keypress(function(e) { + if (e.which === 13) { + $('#searchCitesphereBtn').click(); + } + }); + + // Handle importing selected references + $('#submitCitesphereReferences').click(function() { + if (citesphereData.selectedReferences.length === 0) { + alert('Please select at least one reference to import.'); + return; + } + + var biblioId = $('#addReferenceTarget').val(); + if (!biblioId) { + alert('Error: Could not determine bibliography block.'); + return; + } + + var moduleId = '[[${module.id}]]'; + var slideId = '[[${slide.id}]]'; + var token = $('input[name="_csrf"]').attr('value'); + + $.ajax({ + url: '/staff/module/' + moduleId + '/slide/' + slideId + '/bibliography/' + biblioId + '/citesphere/import', + type: 'POST', + contentType: 'application/json', + headers: { + 'X-CSRF-Token': token + }, + data: JSON.stringify(citesphereData.selectedReferences), + success: function(response) { + if (response.success) { + alert('Successfully imported ' + response.imported_count + ' references from Citesphere!'); + + // Add the imported references to the bibliography block + var biblioBlock = $('[data-biblio-id="' + biblioId + '"]').closest('[data-biblio-block]'); + var referenceSpace = biblioBlock.find('#referenceSpace'); + + response.references.forEach(function(ref) { + var referenceHtml = '
' + + '

' + + 'Type: ' + (ref.type || '') + ', ' + + 'Reference Title: ' + (ref.title || '') + ', ' + + 'Author: ' + (ref.author || '') + ', ' + + 'Year: ' + (ref.year || '') + ', ' + + 'Journal: ' + (ref.journal || '') + ', ' + + 'Url: ' + (ref.url || '') + ', ' + + 'Volume: ' + (ref.volume || '') + ', ' + + 'Issue: ' + (ref.issue || '') + ', ' + + 'Pages: ' + (ref.pages || '') + ', ' + + 'Editors: ' + (ref.editors || '') + ', ' + + 'Note: ' + (ref.note || '') + '' + + '

' + + '' + + '' + + '' + + '' + + '
'; + + var refElement = $(referenceHtml); + refElement.mouseenter(onMouseEnter).mouseleave(onMouseLeave).dblclick(referenceBlockDoubleClick); + referenceSpace.append(refElement); + }); + + // Reset and close modal + citesphereData.selectedReferences = []; + updateSelectedCount(); + $('#addReferenceAlert').modal('hide'); + } else { + alert('Error importing references: ' + (response.error || 'Unknown error')); + } + }, + error: function(xhr, status, error) { + console.error('Error importing references:', error); + alert('Error importing references. Please try again.'); + } + }); + }); + + // Clear Citesphere data when modal is hidden + $('#addReferenceAlert').on('hidden.bs.modal', function () { + citesphereData.selectedReferences = []; + updateSelectedCount(); + $('#citesphereCollections').empty(); + $('#citesphereReferences').empty(); + $('#citesphereSearch').val(''); + }); + From 6478a4a936a548657c43cb960c52011e28cbb578 Mon Sep 17 00:00:00 2001 From: Girik1105 Date: Fri, 12 Sep 2025 14:33:21 -0700 Subject: [PATCH 13/31] [VSPC-189] Fixed oauth flow and citesphere controller --- .../diging/vspace/config/SecurityContext.java | 3 ++- .../vspace/web/staff/CitesphereController.java | 18 +++++++++--------- 2 files changed, 11 insertions(+), 10 deletions(-) diff --git a/vspace/src/main/java/edu/asu/diging/vspace/config/SecurityContext.java b/vspace/src/main/java/edu/asu/diging/vspace/config/SecurityContext.java index 494ce166d..8b8cccfed 100644 --- a/vspace/src/main/java/edu/asu/diging/vspace/config/SecurityContext.java +++ b/vspace/src/main/java/edu/asu/diging/vspace/config/SecurityContext.java @@ -51,7 +51,8 @@ protected void configure(HttpSecurity http) throws Exception { .authorizeRequests() // Anyone can access the urls .antMatchers("/", "/exhibit/**", "/api/**", "/resources/**", "/login", - "/logout", "/register", "/reset/**", "/setup/admin", "/404","/preview/**").permitAll() + "/logout", "/register", "/reset/**", "/setup/admin", "/404","/preview/**", + "/staff/citesphere/oauth/**").permitAll() // The rest of the our application is protected. .antMatchers("/users/**", "/admin/**", "/staff/user/**").hasRole("ADMIN") .antMatchers("/staff/**").hasAnyRole("STAFF", "ADMIN") diff --git a/vspace/src/main/java/edu/asu/diging/vspace/web/staff/CitesphereController.java b/vspace/src/main/java/edu/asu/diging/vspace/web/staff/CitesphereController.java index 5e2417db5..1d77a6d38 100644 --- a/vspace/src/main/java/edu/asu/diging/vspace/web/staff/CitesphereController.java +++ b/vspace/src/main/java/edu/asu/diging/vspace/web/staff/CitesphereController.java @@ -55,7 +55,7 @@ public class CitesphereController { /** * Initiate OAuth authorization with Citesphere */ - @RequestMapping(value = "/api/oauth/authorize", method = RequestMethod.GET) + @RequestMapping(value = "/staff/citesphere/oauth/authorize", method = RequestMethod.GET) public String initiateOAuth(HttpSession session, RedirectAttributes redirectAttributes) { logger.info("[DEBUG] Initiating OAuth authorization with Citesphere"); logger.info("[DEBUG] Citesphere API URL: {}", citesphereApiUrl); @@ -73,8 +73,8 @@ public String initiateOAuth(HttpSession session, RedirectAttributes redirectAttr String state = java.util.UUID.randomUUID().toString(); session.setAttribute("citesphere_oauth_state", state); - // Build authorization URL - String baseUrl = citesphereApiUrl.replace("/api", ""); + // Build authorization URL + String baseUrl = citesphereApiUrl; String redirectUri = getCurrentBaseUrl() + "/staff/citesphere/oauth/callback"; logger.info("[DEBUG] Building OAuth URL - Base URL: {}", baseUrl); @@ -153,7 +153,7 @@ public String handleOAuthCallback( /** * Get user groups from Citesphere */ - @RequestMapping(value = "/api/v1/citesphere/groups", method = RequestMethod.GET) + @RequestMapping(value = "/staff/citesphere/groups", method = RequestMethod.GET) @ResponseBody public ResponseEntity> getGroups(HttpSession session) { logger.info("[DEBUG] API call: getGroups() - Fetching user groups from Citesphere"); @@ -176,7 +176,7 @@ public ResponseEntity> getGroups(HttpSession session) { /** * Get collections for a specific group */ - @RequestMapping(value = "/api/v1/citesphere/groups/{groupId}/collections", method = RequestMethod.GET) + @RequestMapping(value = "/staff/citesphere/groups/{groupId}/collections", method = RequestMethod.GET) @ResponseBody public ResponseEntity> getCollections(@PathVariable String groupId, HttpSession session) { logger.info("[DEBUG] API call: getCollections() - Group ID: {}", groupId); @@ -199,7 +199,7 @@ public ResponseEntity> getCollections(@PathVariable String g /** * Get items for a specific collection */ - @RequestMapping(value = "/api/v1/groups/{groupId}/collections/{collectionId}/items", method = RequestMethod.GET) + @RequestMapping(value = "/staff/citesphere/groups/{groupId}/collections/{collectionId}/items", method = RequestMethod.GET) @ResponseBody public ResponseEntity> getCollectionItems( @PathVariable String groupId, @@ -226,7 +226,7 @@ public ResponseEntity> getCollectionItems( /** * Get all items for a specific group */ - @RequestMapping(value = "/api/v1/groups/{groupId}/items", method = RequestMethod.GET) + @RequestMapping(value = "/staff/citesphere/groups/{groupId}/items", method = RequestMethod.GET) @ResponseBody public ResponseEntity> getGroupItems(@PathVariable String groupId, HttpSession session) { logger.info("[DEBUG] API call: getGroupItems() - Group ID: {}", groupId); @@ -249,7 +249,7 @@ public ResponseEntity> getGroupItems(@PathVariable String gr /** * Import selected references from Citesphere to bibliography */ - @RequestMapping(value = "/api/v1/groups/{groupId}/collections/{collectionId}/items/", method = RequestMethod.POST) + @RequestMapping(value = "/staff/module/{moduleId}/slide/{slideId}/bibliography/{biblioId}/citesphere/import", method = RequestMethod.POST) @ResponseBody public ResponseEntity> importCitesphereReferences( @PathVariable String moduleId, @@ -330,7 +330,7 @@ private String exchangeCodeForToken(String code) { try { OkHttpClient client = new OkHttpClient(); String redirectUri = getCurrentBaseUrl() + "/staff/citesphere/oauth/callback"; - String tokenUrl = citesphereApiUrl.replace("/api", "") + "/oauth/token"; + String tokenUrl = citesphereApiUrl + "oauth/token"; logger.info("[DEBUG] Token exchange URL: {}", tokenUrl); logger.info("[DEBUG] Redirect URI: {}", redirectUri); From fe55e98b01f7f0794498e5d04834bd7528b8f031 Mon Sep 17 00:00:00 2001 From: Girik1105 Date: Fri, 12 Sep 2025 14:58:58 -0700 Subject: [PATCH 14/31] [VSPC-189] shows connection status to frontend --- .../views/staff/modules/slides/slide.html | 329 ++++++++++++------ 1 file changed, 230 insertions(+), 99 deletions(-) diff --git a/vspace/src/main/webapp/WEB-INF/views/staff/modules/slides/slide.html b/vspace/src/main/webapp/WEB-INF/views/staff/modules/slides/slide.html index 72a83f2f8..c27581c1f 100644 --- a/vspace/src/main/webapp/WEB-INF/views/staff/modules/slides/slide.html +++ b/vspace/src/main/webapp/WEB-INF/views/staff/modules/slides/slide.html @@ -2673,7 +2673,10 @@ var citesphereData = { selectedReferences: [], groups: [], - collections: {} + collections: {}, + currentStep: 'connection', + currentGroup: null, + currentCollection: null }; // Initialize Citesphere functionality when modal is shown @@ -2681,9 +2684,9 @@ // Show/hide appropriate tab content and buttons based on active tab updateReferenceModalButtons(); - // If Citesphere tab is active, load initial data + // If Citesphere tab is active, initialize step-by-step flow if ($('#citesphereRefTab').hasClass('active')) { - loadCitesphereGroups(); + initializeCitesphereSteps(); } }); @@ -2691,11 +2694,20 @@ $('a[data-toggle="tab"]').on('shown.bs.tab', function (e) { updateReferenceModalButtons(); - // Load Citesphere data when switching to Citesphere tab + // Initialize Citesphere step-by-step flow when switching to tab if ($(e.target).attr('href') === '#citesphereRefTab') { - loadCitesphereGroups(); + console.log('Citesphere tab activated - initializing steps'); + initializeCitesphereSteps(); } }); + + // Also trigger when tab link is clicked + $(document).on('click', 'a[href="#citesphereRefTab"]', function() { + setTimeout(function() { + console.log('Citesphere tab clicked - initializing steps'); + initializeCitesphereSteps(); + }, 100); + }); function updateReferenceModalButtons() { var isCitesphereTab = $('#citesphereRefTab').hasClass('active'); @@ -2709,143 +2721,223 @@ } } - // Load user groups from Citesphere - function loadCitesphereGroups() { + // Initialize the step-by-step Citesphere flow + function initializeCitesphereSteps() { + console.log('initializeCitesphereSteps called'); + + // Reset step data + citesphereData.currentStep = 'connection'; + citesphereData.currentGroup = null; + citesphereData.currentCollection = null; + citesphereData.selectedReferences = []; + + // Hide all steps initially + $('.citesphere-step').hide(); + console.log('Hidden all steps'); + + // Show a loading message first + $('#connectionStatusContent').html( + '
' + + ' Checking Citesphere connection...' + + '
' + ); + + // Check connection status and show appropriate step + checkCitesphereConnection(); + } + + // Check if user is connected to Citesphere + function checkCitesphereConnection() { + console.log('checkCitesphereConnection called'); var token = $('input[name="_csrf"]').attr('value'); $.ajax({ - url: '/staff/citesphere/groups', + url: '/vspace/staff/citesphere/groups', type: 'GET', headers: { 'X-CSRF-Token': token }, success: function(data) { - displayCitesphereGroups(data); + console.log('Connection successful, received data:', data); + // Connected successfully + showConnectionStatus(true); citesphereData.groups = data; + citesphereData.currentStep = 'groups'; + showStep1Groups(data); }, error: function(xhr, status, error) { - console.error('Error loading Citesphere groups:', error); - if (xhr.status === 500 && xhr.responseText && xhr.responseText.includes('access token')) { - // User needs to authenticate - $('#citesphereCollections').html( - '
' + - '
Authentication Required
' + - '

You need to authenticate with Citesphere to access your bibliography data.

' + - '' + - ' Connect to Citesphere' + - '' + - '
' - ); - } else { - $('#citesphereCollections').html('
Error loading groups. Please check your Citesphere credentials.
'); - } + console.log('Connection failed:', xhr.status, error); + // Not connected or error + showConnectionStatus(false); + citesphereData.currentStep = 'connection'; } }); } + + // Show connection status at the top + function showConnectionStatus(isConnected) { + var statusContainer = $('#connectionStatusContent'); + + if (isConnected) { + statusContainer.html( + '
' + + ' Connected to Citesphere' + + '
' + ); + } else { + statusContainer.html( + '
' + + ' Not Connected to Citesphere
' + + 'You need to authenticate to access your bibliography data.
' + + '' + + ' Connect to Citesphere' + + '' + + '
' + ); + } + } + + // Show Step 1: Groups + function showStep1Groups(groupsData) { + $('#step1-groups').show(); + displayCitesphereGroups(groupsData); + } - // Display groups and collections + // Display groups in Step 1 function displayCitesphereGroups(groupsData) { - var collectionsContainer = $('#citesphereCollections'); - collectionsContainer.empty(); + var groupsContainer = $('#citesphereGroups'); + groupsContainer.empty(); if (!groupsData || !groupsData.data || !Array.isArray(groupsData.data)) { - collectionsContainer.html('
No groups found.
'); + groupsContainer.html('
No groups found.
'); return; } groupsData.data.forEach(function(group) { - var groupElement = $('
' + + var groupElement = $('
' + + '
' + + '
' + '
' + (group.name || 'Unnamed Group') + '
' + - 'Click to load collections' + + 'Click to view collections' + + '
' + + '' + + '
' + '
'); + // Hover effect + groupElement.hover( + function() { $(this).css('background', '#e3f2fd'); }, + function() { $(this).css('background', '#f8f9fa'); } + ); + groupElement.click(function() { - loadGroupCollections(group.zoteroGroupId || group.id, group.name); + citesphereData.currentGroup = group; + showStep2Collections(group); }); - collectionsContainer.append(groupElement); + groupsContainer.append(groupElement); }); } + // Show Step 2: Collections + function showStep2Collections(group) { + $('#step1-groups').hide(); + $('#step2-collections').show(); + $('#currentGroupName').text('Group: ' + (group.name || 'Unnamed Group')); + citesphereData.currentStep = 'collections'; + + loadGroupCollections(group.zoteroGroupId || group.id, group.name); + } + // Load collections for a specific group function loadGroupCollections(groupId, groupName) { var token = $('input[name="_csrf"]').attr('value'); $.ajax({ - url: '/staff/citesphere/groups/' + groupId + '/collections', + url: '/vspace/staff/citesphere/groups/' + groupId + '/collections', type: 'GET', headers: { 'X-CSRF-Token': token }, success: function(data) { displayCollections(data, groupId, groupName); - loadGroupItems(groupId, groupName); // Also load individual items }, error: function(xhr, status, error) { console.error('Error loading collections for group ' + groupId + ':', error); + $('#citesphereCollections').html('
Error loading collections.
'); } }); } - // Display collections for a group + // Display collections for a group in Step 2 function displayCollections(collectionsData, groupId, groupName) { var collectionsContainer = $('#citesphereCollections'); collectionsContainer.empty(); - // Add back button - var backButton = $(''); - backButton.click(function() { - loadCitesphereGroups(); - }); - collectionsContainer.append(backButton); - - var groupHeader = $('
' + groupName + ' - Collections
'); - collectionsContainer.append(groupHeader); - if (!collectionsData || !collectionsData.data || !Array.isArray(collectionsData.data)) { - collectionsContainer.append('
No collections found in this group.
'); + collectionsContainer.html('
No collections found in this group.
'); return; } collectionsData.data.forEach(function(collection) { - var collectionElement = $('
' + - '
' + (collection.data.name || 'Unnamed Collection') + '
' + - 'Click to load items' + + var collectionElement = $('
' + + '
' + + '
' + + '
' + (collection.data.name || 'Unnamed Collection') + '
' + + 'Click to view references' + + '
' + + '' + + '
' + '
'); + // Hover effect + collectionElement.hover( + function() { $(this).css('background', '#e3f2fd'); }, + function() { $(this).css('background', '#f8f9fa'); } + ); + collectionElement.click(function() { - loadCollectionItems(groupId, collection.data.key, collection.data.name); + citesphereData.currentCollection = collection; + showStep3References(groupId, collection.data.key, collection.data.name); }); collectionsContainer.append(collectionElement); }); } - // Load individual items for a group (not in collections) - function loadGroupItems(groupId, groupName) { - var token = $('input[name="_csrf"]').attr('value'); + // Show Step 3: References + function showStep3References(groupId, collectionId, collectionName) { + $('#step2-collections').hide(); + $('#step3-references').show(); + $('#currentCollectionName').text('Collection: ' + (collectionName || 'Unnamed Collection')); + citesphereData.currentStep = 'references'; - $.ajax({ - url: '/staff/citesphere/groups/' + groupId + '/items', - type: 'GET', - headers: { - 'X-CSRF-Token': token - }, - success: function(data) { - displayIndividualItems(data, 'Individual Items in ' + groupName); - }, - error: function(xhr, status, error) { - console.error('Error loading group items:', error); - } - }); + loadCollectionItems(groupId, collectionId, collectionName); } + + // Navigation handlers + $(document).on('click', '#backToGroups', function() { + $('#step2-collections').hide(); + $('#step1-groups').show(); + citesphereData.currentStep = 'groups'; + citesphereData.currentGroup = null; + }); + + $(document).on('click', '#backToCollections', function() { + $('#step3-references').hide(); + $('#step2-collections').show(); + citesphereData.currentStep = 'collections'; + citesphereData.currentCollection = null; + citesphereData.selectedReferences = []; + updateSelectedCount(); + }); // Load items from a specific collection function loadCollectionItems(groupId, collectionId, collectionName) { var token = $('input[name="_csrf"]').attr('value'); $.ajax({ - url: '/staff/citesphere/groups/' + groupId + '/collections/' + collectionId + '/items', + url: '/vspace/staff/citesphere/groups/' + groupId + '/collections/' + collectionId + '/items', type: 'GET', headers: { 'X-CSRF-Token': token @@ -2992,7 +3084,7 @@ var token = $('input[name="_csrf"]').attr('value'); $.ajax({ - url: '/staff/module/' + moduleId + '/slide/' + slideId + '/bibliography/' + biblioId + '/citesphere/import', + url: '/vspace/staff/module/' + moduleId + '/slide/' + slideId + '/bibliography/' + biblioId + '/citesphere/import', type: 'POST', contentType: 'application/json', headers: { @@ -3050,11 +3142,21 @@ // Clear Citesphere data when modal is hidden $('#addReferenceAlert').on('hidden.bs.modal', function () { + // Reset Citesphere data citesphereData.selectedReferences = []; + citesphereData.currentStep = 'connection'; + citesphereData.currentGroup = null; + citesphereData.currentCollection = null; updateSelectedCount(); + + // Clear all containers + $('#citesphereGroups').empty(); $('#citesphereCollections').empty(); $('#citesphereReferences').empty(); - $('#citesphereSearch').val(''); + $('#connectionStatusContent').empty(); + + // Hide all steps + $('.citesphere-step').hide(); }); @@ -3679,45 +3781,74 @@