package org.gcube.gcat.persistence.ckan; import java.io.StringWriter; import java.util.ArrayList; import java.util.HashMap; import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Set; import java.util.regex.Matcher; import java.util.regex.Pattern; import javax.ws.rs.BadRequestException; import javax.ws.rs.ForbiddenException; import javax.ws.rs.InternalServerErrorException; import javax.ws.rs.NotAllowedException; import javax.ws.rs.WebApplicationException; import javax.ws.rs.core.MultivaluedMap; import javax.ws.rs.core.Response.Status; import org.apache.http.MethodNotSupportedException; import org.gcube.com.fasterxml.jackson.databind.JsonNode; import org.gcube.com.fasterxml.jackson.databind.node.ArrayNode; import org.gcube.com.fasterxml.jackson.databind.node.ObjectNode; import org.gcube.common.scope.impl.ScopeBean; import org.gcube.common.scope.impl.ScopeBean.Type; import org.gcube.gcat.api.CMItemStatus; import org.gcube.gcat.api.CMItemVisibility; import org.gcube.gcat.api.GCatConstants; import org.gcube.gcat.api.Role; import org.gcube.gcat.oldutils.Validator; import org.gcube.gcat.profile.MetadataUtility; import org.gcube.gcat.social.PortalUser; import org.gcube.gcat.social.SocialPost; import org.gcube.gcat.utils.URIResolver; import org.gcube.gcat.zulip.ZulipStream; import org.slf4j.Logger; import org.slf4j.LoggerFactory; /** * @author Luca Frosini (ISTI - CNR) */ public class CKANPackage extends CKAN { private static final Logger logger = LoggerFactory.getLogger(CKANPackage.class); /* // see https://docs.ckan.org/en/latest/api/#ckan.logic.action.get.package_list public static final String ITEM_LIST = CKAN.CKAN_API_PATH + "package_list"; */ // see https://docs.ckan.org/en/latest/api/index.html#ckan.logic.action.get.package_search public static final String ITEM_LIST = CKAN.CKAN_API_PATH + "package_search"; // see https://docs.ckan.org/en/latest/api/#ckan.logic.action.create.package_create public static final String ITEM_CREATE = CKAN.CKAN_API_PATH + "package_create"; // see https://docs.ckan.org/en/latest/api/#ckan.logic.action.get.package_show public static final String ITEM_SHOW = CKAN.CKAN_API_PATH + "package_show"; // see https://docs.ckan.org/en/latest/api/#ckan.logic.action.update.package_update public static final String ITEM_UPDATE = CKAN.CKAN_API_PATH + "package_update"; // see https://docs.ckan.org/en/latest/api/#ckan.logic.action.patch.package_patch public static final String ITEM_PATCH = CKAN.CKAN_API_PATH + "package_patch"; // see https://docs.ckan.org/en/latest/api/#ckan.logic.action.delete.package_delete public static final String ITEM_DELETE = CKAN.CKAN_API_PATH + "package_delete"; // see https://docs.ckan.org/en/latest/api/#ckan.logic.action.delete.dataset_purge public static final String ITEM_PURGE = CKAN.CKAN_API_PATH + "dataset_purge"; // limit in https://docs.ckan.org/en/latest/api/index.html#ckan.logic.action.get.package_search protected static final String ROWS_KEY = "rows"; // offset in https://docs.ckan.org/en/latest/api/index.html#ckan.logic.action.get.package_search protected static final String START_KEY = "start"; protected static final String ORGANIZATION_FILTER_TEMPLATE = GCatConstants.ORGANIZATION_PARAMETER + ":%s"; protected static final String ORGANIZATION_REGEX = GCatConstants.ORGANIZATION_PARAMETER + ":[a-zA-Z0-9_\"]*"; protected static final Pattern ORGANIZATION_REGEX_PATTERN; static { ORGANIZATION_REGEX_PATTERN = Pattern.compile(ORGANIZATION_REGEX); } protected static final String LICENSE_KEY = "license_id"; protected static final String EXTRAS_ITEM_URL_KEY = "Item URL"; protected static final String AUTHOR_KEY = "author"; protected static final String AUTHOR_EMAIL_KEY = "author_email"; protected static final String MAINTAINER_KEY = "maintainer"; protected static final String MAINTAINER_EMAIL_KEY = "maintainer_email"; protected static final String OWNER_ORG_KEY = "owner_org"; protected static final String RESOURCES_KEY = "resources"; protected static final String TITLE_KEY = "title"; public static final String EXTRAS_KEY = "extras"; public static final String EXTRAS_KEY_KEY = "key"; public static final String EXTRAS_KEY_VALUE_SYSTEM_TYPE = "system:type"; public static final String EXTRAS_VALUE_KEY = "value"; // The 'results' array is included in the 'result' object for package_search private static final String RESULTS_KEY = "results"; protected static final String PRIVATE_KEY = "private"; protected static final String SEARCHABLE_KEY = "searchable"; protected static final String CAPACITY_KEY = "capacity"; protected static final String CM_STATUS_QUERY_FILTER_KEY = "extras_systemcm_item_status"; protected static final String INCLUDE_PRIVATE_KEY = "include_private"; // protected static final String INCLUDE_DRAFTS_KEY = "include_drafts"; public static final String GROUPS_KEY = "groups"; public static final String TAGS_KEY = "tags"; protected final List managedResources; protected String itemID; protected final CKANUser ckanUser; protected final CKANInstance ckanInstance; protected final Set supportedOrganizations; protected String moderationMessage; protected ZulipStream zulipStream; public CKANPackage() { super(); LIST = ITEM_LIST; CREATE = ITEM_CREATE; READ = ITEM_SHOW; UPDATE = ITEM_UPDATE; PATCH = ITEM_PATCH; DELETE = ITEM_DELETE; PURGE = ITEM_PURGE; managedResources = new ArrayList(); ckanUser = CKANUserCache.getCurrrentCKANUser(); ckanInstance = CKANInstance.getInstance(); supportedOrganizations = ckanInstance.getSupportedOrganizations(); for(String supportedOrganization : supportedOrganizations) { ckanUser.addUserToOrganization(supportedOrganization); } } /* * Return the CKAN organization name using the current context name */ protected static String getOrganizationName(ScopeBean scopeBean) { String contextName = scopeBean.name(); return contextName.toLowerCase().replace(" ", "_"); } protected CKANOrganization checkGotOrganization(String gotOrganization) throws ForbiddenException { if(!supportedOrganizations.contains(gotOrganization)) { String error = String.format( "IS Configuration does not allow to publish in %s organizations. Allowed organization are: %s", gotOrganization, supportedOrganizations); throw new ForbiddenException(error); } CKANOrganization ckanOrganization = new CKANOrganization(); ckanOrganization.setName(gotOrganization); ckanOrganization.read(); return ckanOrganization; } protected CKANOrganization getPublishingOrganization(ObjectNode objectNode) throws ForbiddenException { CKANOrganization ckanOrganization = null; if(objectNode.has(OWNER_ORG_KEY)) { String gotOrganizationName = objectNode.get(OWNER_ORG_KEY).asText(); ckanOrganization = checkGotOrganization(gotOrganizationName); } if(ckanOrganization == null) { // owner organization must be specified if the token belongs to a VRE String organizationFromContext = ckanInstance.getCurrentOrganizationName(); ckanOrganization = checkGotOrganization(organizationFromContext); objectNode.put(OWNER_ORG_KEY, organizationFromContext); } return ckanOrganization; } public ObjectNode checkBaseInformation(String json) throws Exception { return checkBaseInformation(json, false); } public JsonNode cleanResult(JsonNode jsonNode) { if(jsonNode.has(OWNER_ORG_KEY)) { ((ObjectNode) jsonNode).remove(OWNER_ORG_KEY); } return jsonNode; } /** * @param json The json to check * @param allowPartialInfo used for patch method which provide only partial information (i.e. the info to patch) * @return ObjectNode from json * @throws Exception */ public ObjectNode checkBaseInformation(String json, boolean allowPartialInfo) throws Exception { ObjectNode objectNode = (ObjectNode) mapper.readTree(json); try { objectNode = (ObjectNode) checkName(objectNode); }catch (Exception e) { if(!allowPartialInfo) { throw e; } } // We need to enforce the itemID to properly manage resource persistence if(objectNode.has(ID_KEY)) { itemID = objectNode.get(ID_KEY).asText(); } // To include private item in search result (e.g. listing) a private package must be searchable // The package it is just included in the search and listing results but remain private and cannot be accessed // if not authorized if(objectNode.has(PRIVATE_KEY)) { boolean privatePackage = objectNode.get(PRIVATE_KEY).asBoolean(); if(privatePackage) { objectNode.put(SEARCHABLE_KEY, true); } } // check license String licenseId = null; if(objectNode.has(LICENSE_KEY)) { licenseId = objectNode.get(LICENSE_KEY).asText(); } if(licenseId!=null && !licenseId.isEmpty()) { try { CKANLicense.checkLicenseId(licenseId); } catch(Exception e) { throw new BadRequestException( "You must specify an existing license identifier for the item. License list can be retrieved using licence collection"); } }else if(!allowPartialInfo) { throw new BadRequestException( "You must specify a license identifier for the item. License list can be retrieved using licence collection"); } if(objectNode.has(CAPACITY_KEY)) { /* * When a client provides the 'capacity' field as 'private', the item is not counted in the * total number of items in the GUI. We want to avoid such a behavior * See https://support.d4science.org/issues/16410 */ objectNode.remove(CAPACITY_KEY); } PortalUser portalUser = ckanUser.getPortalUser(); String authorName = String.format("%s %s", portalUser.getSurname(), portalUser.getName()); if(authorName==null || authorName.compareTo("")==0) { authorName = ckanUser.getName(); } objectNode.put(AUTHOR_KEY, authorName); String authorEmail = portalUser.getEMail(); objectNode.put(AUTHOR_EMAIL_KEY, authorEmail); if(!objectNode.has(MAINTAINER_KEY)) { if(!objectNode.has(MAINTAINER_EMAIL_KEY)) { objectNode.put(MAINTAINER_KEY, authorName); objectNode.put(MAINTAINER_EMAIL_KEY, authorEmail); }else { objectNode.put(MAINTAINER_KEY, objectNode.get(MAINTAINER_EMAIL_KEY).toString()); } } getPublishingOrganization(objectNode); return objectNode; } protected JsonNode validateJson(String json) { try { // check base information (and set them if needed) ObjectNode objectNode = checkBaseInformation(json); // Validating against profiles if any MetadataUtility metadataUtility = new MetadataUtility(); if(!metadataUtility.getMetadataProfiles().isEmpty()) { Validator validator = new Validator(mapper); objectNode = validator.validateAgainstProfile(objectNode, metadataUtility); } return objectNode; } catch(BadRequestException e) { throw e; } catch(Exception e) { throw new BadRequestException(e); } } public int count() { Map parameters = new HashMap<>(); if(uriInfo != null) { MultivaluedMap queryParameters = uriInfo.getQueryParameters(); parameters = checkListParameters(queryParameters, parameters); } int limit = 1; parameters.put(ROWS_KEY, String.valueOf(limit)); int offset = 0; parameters.put(START_KEY, String.valueOf(offset * limit)); if(!parameters.containsKey(GCatConstants.Q_KEY)) { String filter = getFilterForOrganizations(); parameters.put(GCatConstants.Q_KEY, filter); } sendGetRequest(LIST, parameters); int count = result.get(GCatConstants.COUNT_KEY).asInt(); return count; } protected CMItemStatus getRequestedCMItemStatus() { CMItemStatus cmItemStatus = null; try { MultivaluedMap queryParameters = uriInfo.getQueryParameters(); if(queryParameters.containsKey(GCatConstants.CM_ITEM_STATUS_QUERY_PARAMETER)) { String cmItemStatusString = queryParameters.getFirst(GCatConstants.CM_ITEM_STATUS_QUERY_PARAMETER); cmItemStatus = CMItemStatus.getCMItemStatusFromValue(cmItemStatusString); } }catch (Exception e) { cmItemStatus = null; } return cmItemStatus; } @Override public String list(int limit, int offset) { Map parameters = new HashMap<>(); if(limit <= 0) { // According to CKAN documentation // the number of matching rows to return. There is a hard limit of 1000 datasets per query. // see https://docs.ckan.org/en/2.6/api/index.html#ckan.logic.action.get.package_search limit = 1000; } parameters.put(ROWS_KEY, String.valueOf(limit)); if(offset < 0) { offset = 0; } parameters.put(START_KEY, String.valueOf(offset * limit)); if(uriInfo != null) { MultivaluedMap queryParameters = uriInfo.getQueryParameters(); parameters = checkListParameters(queryParameters, parameters); } if(!parameters.containsKey(GCatConstants.Q_KEY)) { String filter = getFilterForOrganizations(); parameters.put(GCatConstants.Q_KEY, filter); } if(isModerationEnabled()) { String q = parameters.get(GCatConstants.Q_KEY); StringBuffer stringBuffer = new StringBuffer(); stringBuffer.append("("); stringBuffer.append(CM_STATUS_QUERY_FILTER_KEY); stringBuffer.append(":"); CMItemStatus cmItemStatus = getRequestedCMItemStatus(); if(!ckanUser.getPortalUser().isCatalogueModerator()) { switch (ckanUser.getRole()) { case ADMIN: break; case EDITOR: if(cmItemStatus!=null && cmItemStatus!=CMItemStatus.APPROVED) { q = String.format("%s AND %s:%s", q, AUTHOR_EMAIL_KEY, ckanUser.getPortalUser().getEMail()); }else{ cmItemStatus = CMItemStatus.APPROVED; } break; case MEMBER: if(cmItemStatus!=null && cmItemStatus!=CMItemStatus.APPROVED) { throw new ForbiddenException("You are only authorized to list " + CMItemStatus.APPROVED.getValue() + " items"); } cmItemStatus = CMItemStatus.APPROVED; break; default: break; } } if(cmItemStatus!=null) { stringBuffer.append(cmItemStatus.getValue()); if(cmItemStatus == CMItemStatus.APPROVED) { stringBuffer.append(" OR (*:* -"); stringBuffer.append(CM_STATUS_QUERY_FILTER_KEY); stringBuffer.append(":[* TO *])"); } stringBuffer.append(")"); q = String.format("%s AND %s", q, stringBuffer.toString()); parameters.put(GCatConstants.Q_KEY, q); } parameters.put(INCLUDE_PRIVATE_KEY, String.valueOf(true)); } return list(parameters); } protected Set checkOrganizationFilter(String q) { Matcher m = ORGANIZATION_REGEX_PATTERN.matcher(q); Set matches = new HashSet<>(); while(m.find()) { matches.add(q.substring(m.start(), m.end()).replace(GCatConstants.ORGANIZATION_PARAMETER+":", "")); } return matches; } protected static String[] allowedListQueryParameters = new String[] {"q", "fq", "fq_list", "sort", /* "facet", "facet.mincount", "facet.limit", "facet.field", */ "include_drafts", "include_private", "ext_bbox"}; protected String getFilterForOrganizations() { StringWriter stringWriter = new StringWriter(); int i=1; for(String organizationName : supportedOrganizations) { stringWriter.append(String.format(GCatConstants.ORGANIZATION_FILTER_TEMPLATE, organizationName)); if(i!=supportedOrganizations.size()) { // Please note that an item can only belong to a single organization. // Hence the query must put supported organizations in OR. stringWriter.append(" OR "); } i++; } return stringWriter.toString(); } protected Map checkListParameters(MultivaluedMap queryParameters, Map parameters) { String q = null; if(queryParameters.containsKey(GCatConstants.Q_KEY)) { q = queryParameters.getFirst(GCatConstants.Q_KEY); } if(q != null) { Set organizations = checkOrganizationFilter(q); if(organizations.size()==0) { // Adding organization filter to q String filter = getFilterForOrganizations(); parameters.put(GCatConstants.Q_KEY, String.format("%s AND %s", q, filter)); }else { organizations.removeAll(this.supportedOrganizations); if(organizations.size()>0) { String error = String.format("It is not possible to query the following organizations %s. Supported organization in this context are %s", organizations.toString(), supportedOrganizations.toString()); throw new ForbiddenException(error); } } } else { String filter = getFilterForOrganizations(); parameters.put(GCatConstants.Q_KEY, filter); } for(String key : allowedListQueryParameters) { if(queryParameters.containsKey(key)) { parameters.put(key, queryParameters.getFirst(key)); } } // parameters.put(INCLUDE_PRIVATE_KEY, String.valueOf(true)); // By default not including draft // parameters.put(INCLUDE_DRAFTS_KEY, String.valueOf(false)); return parameters; } public String list(Map parameters) { sendGetRequest(LIST, parameters); ArrayNode results = (ArrayNode) result.get(RESULTS_KEY); ArrayNode arrayNode = mapper.createArrayNode(); for(JsonNode node : results) { try { String name = node.get(NAME_KEY).asText(); arrayNode.add(name); } catch(Exception e) { try { logger.error("Unable to get the ID of {}. the result will not be included in the result", mapper.writeValueAsString(node)); } catch(Exception ex) { logger.error("", ex); } } } return getAsString(arrayNode); } protected void rollbackManagedResources() { for(CKANResource ckanResource : managedResources) { try { ckanResource.rollback(); } catch(Exception e) { logger.error("Unable to rollback resource {} to the original value", ckanResource.getResourceID()); } } } protected ArrayNode createResources(ArrayNode resourcesToBeCreated) { ArrayNode created = mapper.createArrayNode(); for(JsonNode resourceNode : resourcesToBeCreated) { CKANResource ckanResource = new CKANResource(itemID); String json = ckanResource.create(getAsString(resourceNode)); created.add(getAsJsonNode(json)); managedResources.add(ckanResource); } return created; } protected JsonNode addExtraField(JsonNode jsonNode, String key, String value) { ArrayNode extras = null; boolean found = false; if(jsonNode.has(EXTRAS_KEY)) { extras = (ArrayNode) jsonNode.get(EXTRAS_KEY); for(JsonNode extra : extras) { if(extra.has(EXTRAS_KEY_KEY) && extra.get(EXTRAS_KEY_KEY).asText().compareTo(key) == 0) { ((ObjectNode) extra).put(EXTRAS_VALUE_KEY, value); found = true; break; } } } else { extras = ((ObjectNode) jsonNode).putArray(EXTRAS_KEY); } if(!found) { ObjectNode extra = mapper.createObjectNode(); extra.put(EXTRAS_KEY_KEY, key); extra.put(EXTRAS_VALUE_KEY, value); extras.add(extra); } return jsonNode; } protected String addItemURLViaResolver(JsonNode jsonNode) { // Adding Item URL via Resolver URIResolver uriResolver = new URIResolver(); String catalogueItemURL = uriResolver.getCatalogueItemURL(name); addExtraField(jsonNode, EXTRAS_ITEM_URL_KEY, catalogueItemURL); return catalogueItemURL; } protected void sendSocialPost(String title, String catalogueItemURL) { try { boolean makePost = false; try { MultivaluedMap queryParameters = uriInfo.getQueryParameters(); if(queryParameters.containsKey(GCatConstants.SOCIAL_POST_QUERY_PARAMETER)) { makePost = Boolean.parseBoolean(queryParameters.getFirst(GCatConstants.SOCIAL_POST_QUERY_PARAMETER)); } } catch(Exception e) { makePost = false; } if(makePost) { ArrayNode arrayNode = (ArrayNode) result.get(TAGS_KEY); SocialPost socialPost = new SocialPost(); socialPost.setItemID(itemID); socialPost.setItemURL(catalogueItemURL); socialPost.setTags(arrayNode); socialPost.setItemTitle(title); Boolean notification = null; try { MultivaluedMap queryParameters = uriInfo.getQueryParameters(); if(queryParameters.containsKey(GCatConstants.SOCIAL_POST_NOTIFICATION_QUERY_PARAMETER)) { notification = Boolean.parseBoolean(queryParameters.getFirst(GCatConstants.SOCIAL_POST_NOTIFICATION_QUERY_PARAMETER)); } } catch(Exception e) { } socialPost.setNotification(notification); socialPost.start(); } else { logger.info("The request explicitly disabled the Social Post."); } } catch(Exception e) { logger.warn( "error dealing with Social Post. The service will not raise the exception belove. Please contact the administrator to let him know about this message.", e); } } protected CMItemStatus getCMItemStatus() throws Exception { String cmItemStatusString = CMItemStatus.APPROVED.getValue(); boolean found = false; if(result.has(EXTRAS_KEY)) { ArrayNode extras = (ArrayNode) result.get(EXTRAS_KEY); for(JsonNode extra : extras) { if(extra.has(EXTRAS_KEY_KEY) && extra.get(EXTRAS_KEY_KEY).asText().compareTo(GCatConstants.SYSTEM_CM_ITEM_STATUS) == 0) { cmItemStatusString = extra.get(EXTRAS_VALUE_KEY).asText(); found = true; break; } } } CMItemStatus cmItemStatus = CMItemStatus.getCMItemStatusFromValue(cmItemStatusString); if(!found) { // The item was published before activating the moderation. // The item is considered as approved and the item representation must be updated setToApproved(result); String ret = sendPostRequest(ITEM_PATCH, getAsString(result)); result = mapper.readTree(ret); result = cleanResult(result); } return cmItemStatus; } protected boolean isItemCreator() { return result.get(AUTHOR_EMAIL_KEY).asText().compareTo(ckanUser.getPortalUser().getEMail())==0; } @Override public String read() { try { String ret = super.read(); result = mapper.readTree(ret); result = cleanResult(result); if(isModerationEnabled()) { CMItemStatus cmItemStatus = getCMItemStatus(); if(cmItemStatus == CMItemStatus.APPROVED) { return getAsString(result); } PortalUser portalUser = ckanUser.getPortalUser(); if(isItemCreator()) { // The author is entitled to read its own items independently from the status return getAsString(result); } if(ckanUser.getRole() == Role.ADMIN || portalUser.isCatalogueModerator()) { // Catalogue-Admin and Catalogue-Moderator are entitled to read items with any statues return getAsString(result); } throw new ForbiddenException("You are not entitled to read a " + cmItemStatus.getValue() + " item"); } return getAsString(result); } catch(WebApplicationException e) { throw e; } catch(Exception e) { throw new InternalServerErrorException(e); } } protected void setToPending(JsonNode jsonNode) { addExtraField(jsonNode, GCatConstants.SYSTEM_CM_ITEM_STATUS, CMItemStatus.PENDING.getValue()); CMItemVisibility cmItemVisibility = CMItemVisibility.PUBLIC; if(jsonNode.has(PRIVATE_KEY)) { boolean privatePackage = jsonNode.get(PRIVATE_KEY).asBoolean(); if(privatePackage) { cmItemVisibility = CMItemVisibility.RESTRICTED; } } addExtraField(jsonNode, GCatConstants.SYSTEM_CM_ITEM_VISIBILITY, cmItemVisibility.getValue()); ((ObjectNode) jsonNode).put(PRIVATE_KEY, true); ((ObjectNode) jsonNode).put(SEARCHABLE_KEY, false); } // see https://docs.ckan.org/en/latest/api/#ckan.logic.action.create.package_create @Override public String create(String json) { try { logger.debug("Going to create Item {}", json); if(ckanUser.getRole().ordinal() < Role.EDITOR.ordinal()) { StringBuffer stringBuffer = new StringBuffer(); stringBuffer.append("Only "); stringBuffer.append(Role.EDITOR.getPortalRole()); stringBuffer.append(" and "); stringBuffer.append(Role.ADMIN.getPortalRole()); stringBuffer.append(" are entitled to create items. "); stringBuffer.append("Please contact the VRE Manager to request your grant."); throw new ForbiddenException(stringBuffer.toString()); } JsonNode jsonNode = validateJson(json); if(isModerationEnabled()) { setToPending(jsonNode); } ArrayNode resourcesToBeCreated = mapper.createArrayNode(); if(jsonNode.has(RESOURCES_KEY)) { resourcesToBeCreated = (ArrayNode) jsonNode.get(RESOURCES_KEY); ((ObjectNode) jsonNode).remove(RESOURCES_KEY); } String catalogueItemURL = ""; if(ckanInstance.getCurrentScopeBean().is(Type.VRE)) { catalogueItemURL = addItemURLViaResolver(jsonNode); } super.create(getAsString(jsonNode)); this.itemID = result.get(ID_KEY).asText(); ArrayNode created = createResources(resourcesToBeCreated); ((ObjectNode) result).replace(RESOURCES_KEY, created); // Actions performed after a package has been correctly created on ckan. String title = result.get(TITLE_KEY).asText(); if(ckanInstance.getCurrentScopeBean().is(Type.VRE)) { sendSocialPost(title, catalogueItemURL); } if(isModerationEnabled()) { createNewStream(); postItemCreatedToStream(); } result = cleanResult(result); return getAsString(result); } catch(WebApplicationException e) { rollbackManagedResources(); throw e; } catch(Exception e) { rollbackManagedResources(); throw new InternalServerErrorException(e); } } // see https://docs.ckan.org/en/latest/api/#ckan.logic.action.update.package_update @Override public String update(String json) { try { JsonNode jsonNode = validateJson(json); read(); this.itemID = result.get(ID_KEY).asText(); jsonNode = checkModerationUpdate(jsonNode); Map originalResources = new HashMap<>(); ArrayNode originalResourcesarrayNode = (ArrayNode) result.get(RESOURCES_KEY); if(originalResources != null) { for(JsonNode resourceNode : originalResourcesarrayNode) { CKANResource ckanResource = new CKANResource(itemID); ckanResource.setPreviousRepresentation(resourceNode); String resourceID = ckanResource.getResourceID(); originalResources.put(resourceID, ckanResource); } } if(jsonNode.has(RESOURCES_KEY)) { ArrayNode resourcesToBeSend = mapper.createArrayNode(); ArrayNode receivedResources = (ArrayNode) jsonNode.get(RESOURCES_KEY); for(JsonNode resourceNode : receivedResources) { CKANResource ckanResource = new CKANResource(itemID); String resourceId = CKANResource.extractResourceID(resourceNode); if(resourceId != null) { if(originalResources.containsKey(resourceId)) { ckanResource = originalResources.get(resourceId); originalResources.remove(resourceId); } else { throw new BadRequestException( "The content contains a resource with id " + resourceId + " which does not exists"); } } if(originalResources.get(resourceId) != null && (!originalResources.get(resourceId).getPreviousRepresentation().equals(resourceNode))) { resourceNode = ckanResource.createOrUpdate(resourceNode); } resourcesToBeSend.add(resourceNode); managedResources.add(ckanResource); } ((ObjectNode) jsonNode).replace(RESOURCES_KEY, resourcesToBeSend); } addItemURLViaResolver(jsonNode); sendPostRequest(ITEM_UPDATE, getAsString(jsonNode)); for(String resourceId : originalResources.keySet()) { CKANResource ckanResource = originalResources.get(resourceId); ckanResource.deleteFile(); } result = cleanResult(result); return getAsString(result); } catch(WebApplicationException e) { rollbackManagedResources(); throw e; } catch(Exception e) { rollbackManagedResources(); throw new InternalServerErrorException(e); } } // see https://docs.ckan.org/en/latest/api/#ckan.logic.action.patch.package_patch @Override public String patch(String json) { try { read(); JsonNode jsonNode = checkBaseInformation(json, true); this.itemID = result.get(ID_KEY).asText(); ((ObjectNode)jsonNode).put(ID_KEY, this.itemID); jsonNode = checkModerationUpdate(jsonNode); Map originalResources = new HashMap<>(); ArrayNode originalResourcesarrayNode = (ArrayNode) result.get(RESOURCES_KEY); if(originalResources != null) { for(JsonNode resourceNode : originalResourcesarrayNode) { CKANResource ckanResource = new CKANResource(itemID); ckanResource.setPreviousRepresentation(resourceNode); String resourceID = ckanResource.getResourceID(); originalResources.put(resourceID, ckanResource); } } if(jsonNode.has(RESOURCES_KEY)) { ArrayNode resourcesToBeSend = mapper.createArrayNode(); ArrayNode receivedResources = (ArrayNode) jsonNode.get(RESOURCES_KEY); for(JsonNode resourceNode : receivedResources) { CKANResource ckanResource = new CKANResource(itemID); String resourceId = CKANResource.extractResourceID(resourceNode); if(resourceId != null) { if(originalResources.containsKey(resourceId)) { ckanResource = originalResources.get(resourceId); originalResources.remove(resourceId); } else { throw new BadRequestException( "The content contains a resource with id " + resourceId + " which does not exists"); } } if(originalResources.get(resourceId) != null && (!originalResources.get(resourceId).getPreviousRepresentation().equals(resourceNode))) { resourceNode = ckanResource.createOrUpdate(resourceNode); } resourcesToBeSend.add(resourceNode); managedResources.add(ckanResource); } ((ObjectNode) jsonNode).replace(RESOURCES_KEY, resourcesToBeSend); } addItemURLViaResolver(jsonNode); sendPostRequest(ITEM_PATCH, getAsString(jsonNode)); for(String resourceId : originalResources.keySet()) { CKANResource ckanResource = originalResources.get(resourceId); ckanResource.deleteFile(); } result = cleanResult(result); return getAsString(result); } catch(WebApplicationException e) { rollbackManagedResources(); throw e; } catch(Exception e) { rollbackManagedResources(); throw new InternalServerErrorException(e); } } @Override protected void delete() { if(isModerationEnabled()) { // TODO } super.delete(); } @Override public void purge() { try { delete(); } catch(WebApplicationException e) { // If the item was deleted but not purged we obtain Not Found. This is accepted. The item has to be purged // with SysAdmin right. Status status = Status.fromStatusCode(e.getResponse().getStatusInfo().getStatusCode()); if(status != Status.NOT_FOUND) { throw e; } } setApiKey(CKANUtility.getSysAdminAPI()); read(); if(result.has(RESOURCES_KEY)) { itemID = result.get(ID_KEY).asText(); ArrayNode arrayNode = (ArrayNode) result.get(RESOURCES_KEY); for(JsonNode jsonNode : arrayNode) { CKANResource ckanResource = new CKANResource(itemID); ckanResource.setPreviousRepresentation(jsonNode); ckanResource.deleteFile(); // Only delete file is required because the item has been deleted } } super.purge(); } /** * Used for bulk purging. Internal use only */ protected void purgeNoCheckNoDeleteFiles() { // setApiKey(CKANUtility.getSysAdminAPI()); // super.purge(); } /* * ---------------------------------------------------------------------------------------- * Moderation Related functions * ---------------------------------------------------------------------------------------- * */ protected boolean isModerationEnabled() { boolean moderationEnabled = ckanInstance.isModerationEnabled(); if(moderationEnabled && zulipStream==null) { zulipStream = new ZulipStream(); } return moderationEnabled; } // private String getItemName() { // String itemName = result.get(name).asText(); // return itemName; // } private void createNewStream() throws Exception { zulipStream.setItemCoordinates(itemID, name); zulipStream.setCKANUser(ckanUser); zulipStream.create(); } private void postItemCreatedToStream() { zulipStream.postItemCreatedToStream(); } protected JsonNode checkModerationUpdate(JsonNode jsonNode) throws Exception{ PortalUser portalUser = ckanUser.getPortalUser(); if(isModerationEnabled()) { CMItemStatus cmItemStatus = getCMItemStatus(); boolean setToPending = true; switch (cmItemStatus) { case APPROVED: if(ckanUser.getRole() != Role.ADMIN) { throw new ForbiddenException("Only " + Role.ADMIN.getPortalRole() + "s are entitled to update an " + cmItemStatus.getValue() + " item"); } setToApproved(jsonNode); setToPending = false; break; case PENDING: if(isItemCreator()) { break; } if(portalUser.isCatalogueModerator()) { break; } throw new ForbiddenException("You are not entitled to update a " + cmItemStatus.getValue() + " item"); case REJECTED: if(isItemCreator()) { break; } if(ckanUser.getRole() == Role.ADMIN || portalUser.isCatalogueModerator()) { break; } throw new ForbiddenException("You are not entitled to update a " + cmItemStatus.getValue() + " item"); default: break; } if(setToPending) { setToPending(jsonNode); } } return jsonNode; } public void setModerationMessage(String moderationMessage) { this.moderationMessage = moderationMessage; } protected void setToRejected(JsonNode jsonNode) { addExtraField(jsonNode, GCatConstants.SYSTEM_CM_ITEM_STATUS, CMItemStatus.REJECTED.getValue()); } protected void setToApproved(JsonNode jsonNode) { ArrayNode extras = (ArrayNode) jsonNode.get(EXTRAS_KEY); boolean approvedSet = false; CMItemVisibility cmItemVisibility = null; for(JsonNode extra : extras) { if(extra.has(EXTRAS_KEY_KEY) && extra.get(EXTRAS_KEY_KEY).asText().compareTo(GCatConstants.SYSTEM_CM_ITEM_STATUS) == 0) { ((ObjectNode) extra).put(EXTRAS_VALUE_KEY, CMItemStatus.APPROVED.getValue()); approvedSet = true; } if(extra.has(EXTRAS_KEY_KEY) && extra.get(EXTRAS_KEY_KEY).asText().compareTo(GCatConstants.SYSTEM_CM_ITEM_VISIBILITY) == 0) { cmItemVisibility = CMItemVisibility.getCMItemStatusFromValue(extra.get(EXTRAS_VALUE_KEY).asText()); } } if(!approvedSet) { addExtraField(jsonNode, GCatConstants.SYSTEM_CM_ITEM_STATUS, CMItemStatus.APPROVED.getValue()); } if(cmItemVisibility==null) { cmItemVisibility = CMItemVisibility.PUBLIC; addExtraField(jsonNode, GCatConstants.SYSTEM_CM_ITEM_VISIBILITY, cmItemVisibility.getValue()); } ((ObjectNode) jsonNode).put(PRIVATE_KEY, cmItemVisibility == CMItemVisibility.RESTRICTED ? true :false); ((ObjectNode) jsonNode).put(SEARCHABLE_KEY, true); } public String approve() { try { if(isModerationEnabled()) { String ret = read(); CMItemStatus cmItemStatus = getCMItemStatus(); switch (cmItemStatus) { case APPROVED: // Nothing TO DO break; case REJECTED: throw new MethodNotSupportedException("You can't approve a rejected item. The item must be updated first. The update will set the item in pending, than it can be approved/rejected."); case PENDING: if(ckanUser.getRole() != Role.ADMIN) { throw new MethodNotSupportedException("Only catalogue moderator can approve a pending item."); } setToApproved(result); ret = sendPostRequest(ITEM_PATCH, getAsString(result)); break; default: break; } result = mapper.readTree(ret); result = cleanResult(result); ZulipStream zulipStream = new ZulipStream(); zulipStream.setItemCoordinates(itemID, name); zulipStream.postItemCreatedToStream(); if(moderationMessage!=null && moderationMessage.compareTo("")!=0) { zulipStream.postMessageToStream(CMItemStatus.PENDING, moderationMessage); } return getAsString(result); } throw new MethodNotSupportedException("The approve operation is available only in moderation mode"); }catch(WebApplicationException e) { throw e; } catch(Exception e) { throw new InternalServerErrorException(e); } } public String reject() { try { if(isModerationEnabled()) { String ret = read(); CMItemStatus cmItemStatus = getCMItemStatus(); switch (cmItemStatus) { case APPROVED: throw new MethodNotSupportedException("You can't rejected an approved item. The item must be updated first. The update will set the item in pending, than it can be approved/rejected."); case REJECTED: // Nothing TO DO break; case PENDING: if(ckanUser.getRole() != Role.ADMIN) { throw new MethodNotSupportedException("Only catalogue moderator can reject a pending item."); } setToRejected(result); ret = sendPostRequest(ITEM_PATCH, getAsString(result)); break; default: break; } result = mapper.readTree(ret); result = cleanResult(result); ZulipStream zulipStream = new ZulipStream(); zulipStream.setItemCoordinates(itemID, name); zulipStream.postItemRejectedToStream(); if(moderationMessage!=null && moderationMessage.compareTo("")!=0) { zulipStream.postMessageToStream(CMItemStatus.REJECTED, moderationMessage); } return getAsString(result); } throw new MethodNotSupportedException("The reject operation is available only in moderation mode"); }catch(WebApplicationException e) { throw e; } catch(Exception e) { throw new InternalServerErrorException(e); } } public void message() { try { if(isModerationEnabled()) { if(moderationMessage==null || moderationMessage.compareTo("")==0) { return; } read(); // Catalogue Moderators are allowed to post message to the dedicated Stream if(!ckanUser.getPortalUser().isCatalogueModerator()) { // Users that are not if(!isItemCreator()) { throw new NotAllowedException("Only item creator and " + GCatConstants.CATALOGUE_MODERATOR + "s are entitled to partecipate to the moderation discussion thread."); } } ZulipStream zulipStream = new ZulipStream(); zulipStream.setItemCoordinates(itemID, name); CMItemStatus cmItemStatus = getCMItemStatus(); zulipStream.postMessageToStream(cmItemStatus, moderationMessage); } throw new MethodNotSupportedException("The message operation is available only in moderation mode"); }catch(WebApplicationException e) { throw e; } catch(Exception e) { throw new InternalServerErrorException(e); } } }