package org.gcube.datacatalogue.ckanutillibrary; import java.sql.Connection; import java.sql.PreparedStatement; import java.sql.ResultSet; import java.sql.SQLException; import java.util.ArrayList; import java.util.HashMap; import java.util.Iterator; import java.util.List; import java.util.Map; import java.util.Map.Entry; import net.htmlparser.jericho.Renderer; import net.htmlparser.jericho.Segment; import net.htmlparser.jericho.Source; import org.apache.commons.dbcp2.BasicDataSource; import org.gcube.datacatalogue.ckanutillibrary.models.CKanUserWrapper; import org.gcube.datacatalogue.ckanutillibrary.models.ResourceBean; import org.gcube.datacatalogue.ckanutillibrary.models.RolesIntoOrganization; import org.gcube.datacatalogue.ckanutillibrary.models.State; import org.gcube.datacatalogue.ckanutillibrary.utils.UtilMethods; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import eu.trentorise.opendata.jackan.CheckedCkanClient; import eu.trentorise.opendata.jackan.CkanClient; import eu.trentorise.opendata.jackan.internal.org.apache.http.HttpResponse; import eu.trentorise.opendata.jackan.internal.org.apache.http.HttpStatus; import eu.trentorise.opendata.jackan.internal.org.apache.http.client.methods.HttpPost; import eu.trentorise.opendata.jackan.internal.org.apache.http.entity.StringEntity; import eu.trentorise.opendata.jackan.internal.org.apache.http.impl.client.CloseableHttpClient; import eu.trentorise.opendata.jackan.internal.org.apache.http.impl.client.HttpClientBuilder; import eu.trentorise.opendata.jackan.model.CkanDataset; import eu.trentorise.opendata.jackan.model.CkanLicense; import eu.trentorise.opendata.jackan.model.CkanOrganization; import eu.trentorise.opendata.jackan.model.CkanPair; import eu.trentorise.opendata.jackan.model.CkanResource; import eu.trentorise.opendata.jackan.model.CkanTag; import eu.trentorise.opendata.jackan.model.CkanUser; /** * This is the Ckan Utils implementation class. * @author Costantino Perciante at ISTI-CNR (costantino.perciante@isti.cnr.it) */ public class CKanUtilsImpl implements CKanUtilsInterface{ private static final Logger logger = LoggerFactory.getLogger(CKanUtilsImpl.class); private String CKAN_CATALOGUE_URL; private String CKAN_DB_NAME; private String CKAN_DB_USER; private String CKAN_DB_PASSWORD; private String CKAN_DB_URL; private Integer CKAN_DB_PORT; private String CKAN_TOKEN_SYS; // use connections pool (multiple threads can use this CKanUtilsImpl instance) private BasicDataSource ds; /** * The ckan catalogue url and database will be discovered in this scope * @param scope * @throws Exception if unable to find datacatalogue info */ public CKanUtilsImpl(String scope) throws Exception{ CKanRunningCluster runningInstance = new CKanRunningCluster(scope); // save information CKAN_DB_URL = runningInstance.getDatabaseHosts().get(0); CKAN_DB_NAME = runningInstance.getDataBaseName(); CKAN_DB_USER = runningInstance.getDataBaseUser(); CKAN_DB_PASSWORD = runningInstance.getDataBasePassword(); logger.debug("Plain password first 3 chars are " + CKAN_DB_PASSWORD.substring(0, 3)); CKAN_TOKEN_SYS = runningInstance.getSysAdminToken(); logger.debug("Plain sys admin token first 3 chars are " + CKAN_TOKEN_SYS.substring(0, 3)); CKAN_DB_PORT = runningInstance.getDatabasePorts().get(0); CKAN_CATALOGUE_URL = runningInstance.getDataCatalogueUrl().get(0); // create connection pool String url = "jdbc:postgresql://" + CKAN_DB_URL + ":" + CKAN_DB_PORT + "/" + CKAN_DB_NAME; ds = new BasicDataSource(); ds.setDriverClassName("org.postgresql.Driver"); ds.setUsername(CKAN_DB_USER); ds.setPassword(CKAN_DB_PASSWORD); ds.setUrl(url); } /** * Retrieve connection from the pool * @return * @throws SQLException */ private Connection getConnection() throws SQLException{ return ds.getConnection(); } @Override public String getApiKeyFromUsername(String username) { logger.debug("Request api key for user = " + username); // in order to avoid errors, the username is always converted String ckanUsername = UtilMethods.fromUsernameToCKanUsername(username); String apiToReturn = null; try{ String query = "SELECT \"apikey\" FROM \"user\" WHERE \"name\"=? and \"state\"=?;"; PreparedStatement preparedStatement = getConnection().prepareStatement(query); preparedStatement.setString(1, ckanUsername); preparedStatement.setString(2, State.ACTIVE.toString().toLowerCase()); ResultSet rs = preparedStatement.executeQuery(); while (rs.next()) { apiToReturn = rs.getString("apikey"); break; } logger.debug("Api key retrieved for user " + ckanUsername); }catch(Exception e){ logger.error("Unable to retrieve key for user " + ckanUsername, e); } return apiToReturn; } @Override public CKanUserWrapper getUserFromApiKey(String apiKey) { logger.debug("Request user whose api key is = " + apiKey); CKanUserWrapper user = new CKanUserWrapper(); try{ String query = "SELECT * FROM \"user\" WHERE \"apikey\"=? and \"state\"=?;"; PreparedStatement preparedStatement = getConnection().prepareStatement(query); preparedStatement.setString(1, apiKey); preparedStatement.setString(2, State.ACTIVE.toString().toLowerCase()); ResultSet rs = preparedStatement.executeQuery(); while (rs.next()) { user.setId(rs.getString("id")); user.setName(rs.getString("name")); user.setApiKey(apiKey); user.setCreationTimestamp(rs.getTimestamp("created").getTime()); user.setAbout(rs.getString("about")); user.setOpenId(rs.getString("openid")); user.setFullName(rs.getString("fullname")); user.setEmail(rs.getString("email")); user.setAdmin(rs.getBoolean("sysadmin")); logger.debug("User retrieved"); break; } }catch(Exception e){ logger.error("Unable to retrieve user with api key " + apiKey, e); } return user; } @Override public List getOrganizationsByUser(String username) { logger.debug("Requested organizations for user " + username); // in order to avoid errors, the username is always converted String ckanUsername = UtilMethods.fromUsernameToCKanUsername(username); String userId = getUserIdByUsername(ckanUsername); // list to return List toReturn = new ArrayList(); // get the CkanClient to retrieve the organization from the id CkanClient client = new CkanClient(CKAN_CATALOGUE_URL); try{ List organizationIds = getOrganizationsIds(); // for each org id, check if the user is included for (String orgId : organizationIds) { String query = "SELECT * FROM \"member\" WHERE \"table_id\"=? and \"group_id\"=? and \"table_name\"=? and \"state\"=?;"; PreparedStatement preparedStatement = getConnection().prepareStatement(query); preparedStatement.setString(1, userId); preparedStatement.setString(2, orgId); preparedStatement.setString(3, "user"); preparedStatement.setString(4, State.ACTIVE.toString().toLowerCase()); ResultSet rs = preparedStatement.executeQuery(); while (rs.next()) { // the role within the organization doesn't matter logger.debug("User " + ckanUsername + " belongs to organization with id " + orgId); toReturn.add(client.getOrganization(orgId)); } } }catch(Exception e){ logger.error("Unable to get user's organizations", e); } return toReturn; } @Override public Map> getGroupsAndRolesByUser( String username, List rolesToMatch) { logger.debug("Requested roles the user " + username + " has into his organizations"); logger.debug("Roles to check are " + rolesToMatch); Map> toReturn = new HashMap>(); // in order to avoid errors, the username is always converted String ckanUsername = UtilMethods.fromUsernameToCKanUsername(username); // retrieve the user and if it is a sys_admin, for every organizations that will be created in the map add also // the sys_admin role boolean isSysAdmin = false; if(rolesToMatch.contains(RolesIntoOrganization.SYSADMIN)){ // get its key String apiKey = getApiKeyFromUsername(ckanUsername); isSysAdmin = isSysAdmin(ckanUsername, apiKey); } try{ // get id from the user String userId = getUserIdByUsername(ckanUsername); // get the id of all the organizations List organizationIds = getOrganizationsIds(); for (String orgId : organizationIds) { // go to the member table, that says which role has this user into the org String query = "SELECT * FROM \"member\" WHERE \"table_id\"=? and \"group_id\"=? and \"table_name\"=? and \"state\"=?;"; PreparedStatement preparedStatement = getConnection().prepareStatement(query); preparedStatement.setString(1, userId); preparedStatement.setString(2, orgId); preparedStatement.setString(3, "user"); preparedStatement.setString(4, State.ACTIVE.toString().toLowerCase()); ResultSet rs = preparedStatement.executeQuery(); // prepare the data to put into the hashmap List rolesIntoOrg = new ArrayList(); if(isSysAdmin) rolesIntoOrg.add(RolesIntoOrganization.SYSADMIN); while(rs.next()){ // check String role = rs.getString("capacity"); if(rolesToMatch.contains(RolesIntoOrganization.valueOf(role))){ rolesIntoOrg.add(RolesIntoOrganization.valueOf(role)); logger.debug("User " + ckanUsername + " has role " + role + " into organization with id " + orgId); } } if(!rolesIntoOrg.isEmpty()) toReturn.put(orgId, rolesIntoOrg); } }catch(Exception e){ logger.error("Unable to analyze user's roles", e); } return toReturn; } /** * Returns the user id given his username * @param username * @return the id on success, null otherwise */ private String getUserIdByUsername(String username) { logger.debug("Request user id whose username is = " + username); // in order to avoid errors, the username is always converted String ckanUsername = UtilMethods.fromUsernameToCKanUsername(username); String userId = null; try{ CkanClient client = new CkanClient(CKAN_CATALOGUE_URL); client.getUser(ckanUsername).getId(); logger.debug("User id retrieved for " + ckanUsername + " "+ userId); }catch(Exception e){ logger.error("Unable to retrieve user with name " + ckanUsername, e); } return userId; } /** * Retrieve the list of organizations ids * @return */ private List getOrganizationsIds(){ List toReturn = new ArrayList(); CkanClient client = new CkanClient(CKAN_CATALOGUE_URL); List orgs = client.getOrganizationList(); for (CkanOrganization ckanOrganization : orgs) { logger.debug("Retrieved org " + ckanOrganization.getName()); toReturn.add(ckanOrganization.getId()); } return toReturn; } @Override public String getCatalogueUrl() { return CKAN_CATALOGUE_URL; } @Override public List getOrganizationsNamesByUser(String username) { logger.debug("Requested organizations for user " + username); // in order to avoid errors, the username is always converted String ckanUsername = UtilMethods.fromUsernameToCKanUsername(username); List orgs = getOrganizationsByUser(ckanUsername); List orgsName = new ArrayList(); for (CkanOrganization ckanOrganization : orgs) { orgsName.add(ckanOrganization.getName()); logger.debug("Organization name is " + ckanOrganization.getName()); } return orgsName; } @Override public String findLicenseIdByLicense(String chosenLicense) { logger.debug("Requested license id"); CkanClient client = new CkanClient(CKAN_CATALOGUE_URL); //retrieve the list of available licenses List licenses = client.getLicenseList(); for (CkanLicense ckanLicense : licenses) { if(ckanLicense.getTitle().equals(chosenLicense)) return ckanLicense.getId(); } return null; } @Override public List getLicenseTitles() { logger.debug("Request for CKAN licenses"); // get the url and the api key of the user List result = new ArrayList(); CkanClient client = new CkanClient(CKAN_CATALOGUE_URL); //retrieve the list of available licenses List licenses = client.getLicenseList(); for (CkanLicense ckanLicense : licenses) { result.add(ckanLicense.getTitle()); logger.debug("License is " + ckanLicense.getTitle() + " and id " + ckanLicense.getId()); } return result; } @Override public boolean setDatasetPrivate(boolean priv, String organizationId, String datasetId, String apiKey) { String pathSetPrivate = "/api/3/action/bulk_update_private"; String pathSetPublic = "/api/3/action/bulk_update_public"; if(apiKey == null || apiKey.isEmpty()){ logger.error("The apiKey parameter is mandatory"); return false; } // Request parameters to be replaced String parameter = "{" + "\"org_id\":\"ORGANIZATION_ID\"," + "\"datasets\":[\"DATASET_ID\"]" + "}"; if(organizationId != null && !organizationId.isEmpty() && datasetId != null && !datasetId.isEmpty()){ // replace with right data parameter = parameter.replace("ORGANIZATION_ID", organizationId); parameter = parameter.replace("DATASET_ID", datasetId); CloseableHttpClient httpClient = HttpClientBuilder.create().build(); if(priv){ try { HttpPost request = new HttpPost(CKAN_CATALOGUE_URL + pathSetPrivate); request.addHeader("Authorization", apiKey); StringEntity params = new StringEntity(parameter); request.setEntity(params); HttpResponse response = httpClient.execute(request); logger.debug("[PRIVATE]Response code is " + response.getStatusLine().getStatusCode() + " and response message is " + response.getStatusLine().getReasonPhrase()); if(response.getStatusLine().getStatusCode() == HttpStatus.SC_OK) return true; }catch (Exception ex) { logger.error("Error while trying to set private the dataset ", ex); } }else { try { HttpPost request = new HttpPost(CKAN_CATALOGUE_URL + pathSetPublic); StringEntity params =new StringEntity(parameter); request.addHeader("Authorization", apiKey); request.setEntity(params); HttpResponse response = httpClient.execute(request); logger.debug("[PUBLIC]Response code is " + response.getStatusLine().getStatusCode() + " and response message is " + response.getStatusLine().getReasonPhrase()); if(response.getStatusLine().getStatusCode() == HttpStatus.SC_OK) return true; }catch (Exception ex) { logger.error("Error while trying to set public the dataset ", ex); } } } return false; } @Override public String addResourceToDataset(ResourceBean resourceBean, String apiKey) { logger.debug("Request to add a resource described by this bean " + resourceBean); try{ if(UtilMethods.resourceExists(resourceBean.getUrl())){ // in order to avoid errors, the username is always converted String ckanUsername = UtilMethods.fromUsernameToCKanUsername(resourceBean.getOwner()); CkanResource resource = new CkanResource(CKAN_CATALOGUE_URL, resourceBean.getDatasetId()); resource.setName(resourceBean.getName()); // escape description Source description = new Source(resourceBean.getDescription()); Segment htmlSeg = new Segment(description, 0, description.length()); Renderer htmlRend = new Renderer(htmlSeg); resource.setDescription(htmlRend.toString()); resource.setUrl(resourceBean.getUrl()); resource.setOwner(ckanUsername); // Checked client CheckedCkanClient client = new CheckedCkanClient(CKAN_CATALOGUE_URL, apiKey); CkanResource createdRes = client.createResource(resource); if(createdRes != null){ logger.debug("Resource " + createdRes.getName() + " is now available"); return createdRes.getId(); } }else logger.error("There is no resource at this url " + resourceBean.getUrl()); }catch(Exception e){ logger.error("Unable to create the resource described by the bean " + resourceBean, e); } return null; } @Override public boolean deleteResourceFromDataset(String resourceId, String apiKey) { logger.error("Request to delete a resource with id " + resourceId + " coming by user with key " + apiKey); try{ CheckedCkanClient client = new CheckedCkanClient(CKAN_CATALOGUE_URL, apiKey); client.deleteResource(resourceId); return true; }catch(Exception e){ logger.error("Unable to delete resource whose id is " + resourceId, e); } return false; } @Override public String createCKanDataset(String apiKey, String withId, String title, String organizationNameOrId, String author, String authorMail, String maintainer, String maintainerMail, long version, String description, String licenseId, List tags, Map customFields, List resources, boolean setPublic) { logger.debug("Request for dataset creation"); CheckedCkanClient client = new CheckedCkanClient(CKAN_CATALOGUE_URL, apiKey); // get client from apiKey String ckanUsername = getUserFromApiKey(apiKey).getName(); // create the base dataset and fill it CkanDataset dataset = new CkanDataset(); // set values dataset.setId(withId); // get the name from the title dataset.setName(UtilMethods.nameFromTitle(title)); dataset.setTitle(title); CkanOrganization orgOwner = client.getOrganization(organizationNameOrId); dataset.setOwnerOrg(orgOwner.getId()); dataset.setAuthor(author); dataset.setAuthorEmail(authorMail); dataset.setMaintainer(maintainer); dataset.setMaintainerEmail(maintainerMail); dataset.setVersion(String.valueOf(version)); // description must be escaped Source descriptionEscaped = new Source(description); Segment htmlSeg = new Segment(descriptionEscaped, 0, descriptionEscaped.length()); Renderer htmlRend = new Renderer(htmlSeg); dataset.setNotes(htmlRend.toString()); logger.debug("Description (escaped is ) " + htmlRend.toString()); dataset.setLicenseId(licenseId); // set the tags, if any if(tags != null && !tags.isEmpty()){ // convert to ckan tags List ckanTags = new ArrayList(tags.size()); for (String stringTag : tags) { ckanTags.add(new CkanTag(stringTag)); } dataset.setTags(ckanTags); } // set the custom fields, if any if(customFields != null && !customFields.isEmpty()){ // iterate and create Iterator> iterator = customFields.entrySet().iterator(); List extras = new ArrayList(customFields.entrySet().size()); while (iterator.hasNext()) { Map.Entry entry = (Map.Entry) iterator.next(); extras.add(new CkanPair(entry.getKey(), entry.getValue())); } dataset.setExtras(extras); } // check if we need to add the resources if(resources != null && !resources.isEmpty()){ logger.debug("We need to add resources to the dataset"); try{ List resourcesCkan = new ArrayList(); for(ResourceBean resource: resources){ CkanResource newResource = new CkanResource(); newResource.setDescription(resource.getDescription()); newResource.setId(resource.getId()); newResource.setUrl(resource.getUrl()); newResource.setName(resource.getName()); newResource.setMimetype(resource.getMimeType()); newResource.setOwner(ckanUsername); resourcesCkan.add(newResource); } // add to the dataset dataset.setResources(resourcesCkan); }catch(Exception e){ logger.error("Unable to add those resources to the dataset", e); } } // try to create CkanDataset res = null; try{ res = client.createDataset(dataset); if(res != null){ logger.debug("Dataset with name " + res.getName() + " has been created. Setting visibility"); // set visibility setDatasetPrivate( !setPublic, // swap to private res.getOrganization().getId(), res.getId(), ckanUsername); return res.getId(); } }catch(Exception e){ // try to update logger.error("Error while creating the dataset.", e); } return null; } @Override public String getUrlFromDatasetIdOrName(String apiKey, String datasetIdOrName) { logger.debug("Request coming for dataset url of dataset with name/id " + datasetIdOrName); // the url of the dataset looks like "getCatalogueUrl() + /dataset/ + dataset name" try{ CheckedCkanClient client = new CheckedCkanClient(CKAN_CATALOGUE_URL, apiKey); CkanDataset dataset = client.getDataset(datasetIdOrName); if(dataset != null){ return CKAN_CATALOGUE_URL + "/dataset/" + dataset.getName(); } }catch(Exception e){ logger.error("Error while retrieving dataset with id/name=" + datasetIdOrName, e); } return null; } @Override public void checkRole(String username, String organizationName, RolesIntoOrganization correspondentRoleToCheck) { logger.debug("Request for checking if " + username + " into " + organizationName + " has role " + correspondentRoleToCheck); if(correspondentRoleToCheck.equals(RolesIntoOrganization.SYSADMIN)){ logger.debug("SYSADMIN role cannot be created programmatically... The user role will be turned into admin"); correspondentRoleToCheck = RolesIntoOrganization.ADMIN; } // convert ckan username String ckanUsername = UtilMethods.fromUsernameToCKanUsername(username); // we need to use the apis to make this String path = "/api/3/action/organization_member_create"; // Request parameters to be replaced String parameter = "{" + "\"id\":\"ORGANIZATION_ID_NAME\"," + "\"username\":\"USERNAME_ID_NAME\"," + "\"role\":\"ROLE\"" + "}"; // replace those values parameter = parameter.replace("ORGANIZATION_ID_NAME", organizationName.toLowerCase()); parameter = parameter.replace("USERNAME_ID_NAME", ckanUsername); parameter = parameter.replace("ROLE", correspondentRoleToCheck.toString().toLowerCase()); logger.debug("API request for organization membership is going to be " + parameter); CloseableHttpClient httpClient = HttpClientBuilder.create().build(); try { HttpPost request = new HttpPost(CKAN_CATALOGUE_URL + path); request.addHeader("Authorization", CKAN_TOKEN_SYS); // sys token StringEntity params = new StringEntity(parameter); request.setEntity(params); HttpResponse response = httpClient.execute(request); logger.debug("Response code is " + response.getStatusLine().getStatusCode() + " and response message is " + response.getStatusLine().getReasonPhrase()); if(response.getStatusLine().getStatusCode() == HttpStatus.SC_OK) return; }catch (Exception ex) { logger.error("Error while trying to change the role for this user ", ex); } return; } @Override public boolean isSysAdmin(String username, String apiKey) { try{ // in order to avoid errors, the username is always converted String ckanUsername = UtilMethods.fromUsernameToCKanUsername(username); CheckedCkanClient checkedClient = new CheckedCkanClient(CKAN_CATALOGUE_URL, apiKey); CkanUser user = checkedClient.getUser(getUserIdByUsername(ckanUsername)); return user.isSysadmin(); }catch(Exception e){ logger.error("Failed to check if the user " + username + " has role sysadmin", e); } return false; } @Override protected void finalize() throws Throwable { logger.debug("Closing connection poolon finalize()"); ds.close(); } }