diff --git a/src/main/java/org/gcube/portlets/widgets/ckandatapublisherwidget/client/ui/dataset/resources/TagsPanel.java b/src/main/java/org/gcube/portlets/widgets/ckandatapublisherwidget/client/ui/dataset/resources/TagsPanel.java index 468cf89..a9aa914 100644 --- a/src/main/java/org/gcube/portlets/widgets/ckandatapublisherwidget/client/ui/dataset/resources/TagsPanel.java +++ b/src/main/java/org/gcube/portlets/widgets/ckandatapublisherwidget/client/ui/dataset/resources/TagsPanel.java @@ -88,7 +88,7 @@ public class TagsPanel extends Composite{ } // ckan accepts only alphanumeric values - String[] subTags = itemBox.getValue().split(" "); + String[] subTags = itemBox.getValue().trim().split(" "); if(subTags.length == 1){ if(!subTags[0].matches(REGEX_TAG)) return; @@ -102,7 +102,7 @@ public class TagsPanel extends Composite{ } } - final String value = itemBox.getValue(); + final String value = itemBox.getValue().trim(); final ListItem displayItem = new ListItem(); displayItem.setStyleName("tag-style"); Span tagText = new Span(itemBox.getValue()); @@ -134,7 +134,7 @@ public class TagsPanel extends Composite{ return; // ckan accepts only alphanumeric values - String[] subTags = tag.split(" "); + String[] subTags = tag.trim().split(" "); if(subTags.length == 1){ if(!subTags[0].matches(REGEX_TAG)) return; @@ -173,7 +173,7 @@ public class TagsPanel extends Composite{ */ private void removeTag(ListItem displayItem, String value) { - tagsList.remove(value); + tagsList.remove(value.trim()); tagsPanel.remove(displayItem); } diff --git a/src/main/java/org/gcube/portlets/widgets/ckandatapublisherwidget/server/CKANPublisherServicesImpl.java b/src/main/java/org/gcube/portlets/widgets/ckandatapublisherwidget/server/CKANPublisherServicesImpl.java index 0c1af27..a2dcb7f 100644 --- a/src/main/java/org/gcube/portlets/widgets/ckandatapublisherwidget/server/CKANPublisherServicesImpl.java +++ b/src/main/java/org/gcube/portlets/widgets/ckandatapublisherwidget/server/CKANPublisherServicesImpl.java @@ -389,10 +389,33 @@ public class CKANPublisherServicesImpl extends RemoteServiceServlet implements C // start a thread that will associate this dataset with the group if(toCreate.getChosenProfile() != null){ - AssociationToGroupThread thread = new AssociationToGroupThread(toCreate.getChosenProfile(), datasetId, userName, utils, organizationNameOrId); - thread.start(); + AssociationToGroupThread threadAssociationToGroup = + new AssociationToGroupThread( + toCreate.getChosenProfile(), + datasetId, + userName, + utils, + organizationNameOrId + ); + threadAssociationToGroup.start(); } + + // ask token for this context + + + // launch notification thread + WritePostCatalogueManagerThread threadWritePost = + new WritePostCatalogueManagerThread( + userName, + scope, + toCreate.getTitle(), + datasetUrl, + false, + toCreate.getTags(), + toCreate.getAuthorFullName() + ); + threadWritePost.start(); return toCreate; diff --git a/src/main/java/org/gcube/portlets/widgets/ckandatapublisherwidget/server/WritePostCatalogueManagerThread.java b/src/main/java/org/gcube/portlets/widgets/ckandatapublisherwidget/server/WritePostCatalogueManagerThread.java new file mode 100644 index 0000000..fc84a71 --- /dev/null +++ b/src/main/java/org/gcube/portlets/widgets/ckandatapublisherwidget/server/WritePostCatalogueManagerThread.java @@ -0,0 +1,89 @@ +package org.gcube.portlets.widgets.ckandatapublisherwidget.server; + +import static org.gcube.common.authorization.client.Constants.authorizationService; + +import java.util.ArrayList; +import java.util.List; + +import org.gcube.common.authorization.library.provider.SecurityTokenProvider; +import org.gcube.common.authorization.library.provider.UserInfo; +import org.gcube.common.scope.api.ScopeProvider; +import org.gcube.portlets.widgets.ckandatapublisherwidget.server.utils.Utils; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + + +/** + * Write a post in a VRe + * @author Costantino Perciante at ISTI-CNR + * (costantino.perciante@isti.cnr.it) + */ +public class WritePostCatalogueManagerThread extends Thread { + + private static final Logger logger = LoggerFactory.getLogger(WritePostCatalogueManagerThread.class); + + private String username; + private String scope; + private String productTitle; + private String productUrl; + private boolean enableNotification; + private List hashtags; + String userFullName; + + /** + * @param token + * @param scope + * @param productTitle + * @param productUrl + * @param enableNotification + * @param hashtags + * @param userFullName + */ + public WritePostCatalogueManagerThread( + String username, String scope, + String productTitle, String productUrl, boolean enableNotification, + List hashtags, String userFullName) { + super(); + this.username = username; + this.scope = scope; + this.productTitle = productTitle; + this.productUrl = productUrl; + this.enableNotification = enableNotification; + this.hashtags = hashtags; + this.userFullName = userFullName; + } + + @Override + public void run() { + + try{ + // evaluate user's token for this scope + String token = authorizationService().generateUserToken(new UserInfo(username, new ArrayList()), scope); + + logger.info("Started request to write application post " + + "for new product created. Scope is " + scope + " and " + + "token is " + token.substring(0, 10) + "****************"); + + // set token and scope + ScopeProvider.instance.set(scope); + SecurityTokenProvider.instance.set(token); + + // write + Utils.writeProductPost( + productTitle, + productUrl, + userFullName, + hashtags, + enableNotification + ); + + }catch(Exception e){ + logger.error("Failed to write the post because of the following error ", e); + }finally{ + // remove token and scope + SecurityTokenProvider.instance.reset(); + ScopeProvider.instance.reset(); + } + } + +} diff --git a/src/main/java/org/gcube/portlets/widgets/ckandatapublisherwidget/server/utils/ServiceEndPointReaderSocial.java b/src/main/java/org/gcube/portlets/widgets/ckandatapublisherwidget/server/utils/ServiceEndPointReaderSocial.java new file mode 100644 index 0000000..3907153 --- /dev/null +++ b/src/main/java/org/gcube/portlets/widgets/ckandatapublisherwidget/server/utils/ServiceEndPointReaderSocial.java @@ -0,0 +1,104 @@ +package org.gcube.portlets.widgets.ckandatapublisherwidget.server.utils; + +import static org.gcube.resources.discovery.icclient.ICFactory.clientFor; +import static org.gcube.resources.discovery.icclient.ICFactory.queryFor; + +import java.util.Iterator; +import java.util.List; + +import org.gcube.common.resources.gcore.ServiceEndpoint; +import org.gcube.common.resources.gcore.ServiceEndpoint.AccessPoint; +import org.gcube.common.scope.api.ScopeProvider; +import org.gcube.resources.discovery.client.api.DiscoveryClient; +import org.gcube.resources.discovery.client.queries.api.SimpleQuery; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * Retrieves the base url of the social-networking service in the scope provided + * @author Costantino Perciante at ISTI-CNR + * (costantino.perciante@isti.cnr.it) + */ +public class ServiceEndPointReaderSocial { + + private String basePath = null; + + private static Logger logger = LoggerFactory.getLogger(ServiceEndPointReaderSocial.class); + private final static String RUNTIME_RESOURCE_NAME = "SocialNetworking"; + private final static String CATEGORY = "Portal"; + + public ServiceEndPointReaderSocial(String context){ + + if(context == null || context.isEmpty()) + throw new IllegalArgumentException("A valid context is needed to discover the service"); + + + String oldContext = ScopeProvider.instance.get(); + ScopeProvider.instance.set(context); + + try{ + + List resources = getConfigurationFromIS(); + if (resources.size() == 0){ + logger.error("There is no Runtime Resource having name " + RUNTIME_RESOURCE_NAME +" and Category " + CATEGORY + " in this scope."); + throw new Exception("There is no Runtime Resource having name " + RUNTIME_RESOURCE_NAME +" and Category " + CATEGORY + " in this scope."); + } + else { + + for (ServiceEndpoint res : resources) { + + Iterator accessPointIterator = res.profile().accessPoints().iterator(); + + while (accessPointIterator.hasNext()) { + ServiceEndpoint.AccessPoint accessPoint = (ServiceEndpoint.AccessPoint) accessPointIterator + .next(); + + // get base path + basePath = accessPoint.address(); + + // break + break; + } + } + + } + + }catch(Exception e){ + + logger.error("Unable to retrieve such service endpoint information!", e); + + }finally{ + + if(oldContext != null && !oldContext.equals(context)) + ScopeProvider.instance.set(oldContext); + + } + + logger.info("Found base path " + basePath + " for the service"); + + } + + /** + * Retrieve endpoints information from IS for the Service endpoint + * @return list of endpoints + * @throws Exception + */ + private List getConfigurationFromIS() throws Exception{ + + SimpleQuery query = queryFor(ServiceEndpoint.class); + query.addCondition("$resource/Profile/Name/text() eq '"+ RUNTIME_RESOURCE_NAME +"'"); + query.addCondition("$resource/Profile/Category/text() eq '"+ CATEGORY +"'"); + DiscoveryClient client = clientFor(ServiceEndpoint.class); + List toReturn = client.submit(query); + return toReturn; + + } + + /** + * Get the base path of the social networking service + * @return + */ + public String getBasePath() { + return basePath; + } +} diff --git a/src/main/java/org/gcube/portlets/widgets/ckandatapublisherwidget/server/utils/Utils.java b/src/main/java/org/gcube/portlets/widgets/ckandatapublisherwidget/server/utils/Utils.java index 8a1531c..ac61f4e 100644 --- a/src/main/java/org/gcube/portlets/widgets/ckandatapublisherwidget/server/utils/Utils.java +++ b/src/main/java/org/gcube/portlets/widgets/ckandatapublisherwidget/server/utils/Utils.java @@ -1,5 +1,8 @@ package org.gcube.portlets.widgets.ckandatapublisherwidget.server.utils; +import java.io.BufferedReader; +import java.io.InputStream; +import java.io.InputStreamReader; import java.util.ArrayList; import java.util.HashMap; import java.util.List; @@ -7,7 +10,14 @@ import java.util.Map; import javax.servlet.http.HttpSession; +import org.apache.http.HttpEntity; +import org.apache.http.HttpResponse; +import org.apache.http.client.methods.HttpPost; +import org.apache.http.entity.StringEntity; +import org.apache.http.impl.client.CloseableHttpClient; +import org.apache.http.impl.client.HttpClientBuilder; import org.gcube.application.framework.core.session.ASLSession; +import org.gcube.common.authorization.library.provider.SecurityTokenProvider; import org.gcube.common.homelibrary.home.exceptions.InternalErrorException; import org.gcube.common.homelibrary.home.workspace.WorkspaceItem; import org.gcube.common.homelibrary.home.workspace.folder.items.GCubeItem; @@ -41,6 +51,8 @@ import org.gcube.vomanagement.usermanagement.model.GCubeRole; import org.gcube.vomanagement.usermanagement.model.GatewayRolesNames; import org.slf4j.LoggerFactory; +import com.fasterxml.jackson.databind.ObjectMapper; + import eu.trentorise.opendata.jackan.model.CkanOrganization; /** @@ -51,6 +63,11 @@ public class Utils { // Logger private static final org.slf4j.Logger logger = LoggerFactory.getLogger(Utils.class); + private static final String APPLICATION_ID_CATALOGUE_MANAGER = "org.gcube.datacatalogue.DataCatalogueManager"; + private static final String NOTIFICATION_MESSAGE = "Dear members,
The product PRODUCT_TITLE has been just published by USER_FULLNAME.
You can find it here: PRODUCT_URL
"; + private static final String SOCIAL_SERVICE_APPLICATION_TOKEN = "2/tokens/generate-application-token/"; + private static final String SOCIAL_SERVICE_WRITE_APPLICATION_POST = "2/posts/write-post-app/"; + private static final String MEDIATYPE_JSON = "application/json"; /** Gets the gcube item properties. * @@ -342,4 +359,121 @@ public class Utils { return beans; } -} + + /** + * Send notification to vre members about the created product by writing a post. + * @param productName the title of the product + * @param productUrl the url of the product + * @param hashtags a list of product's hashtags + */ + public static void writeProductPost(String productName, String productUrl, String userFullname, List hashtags, boolean enablePostNotification){ + + // discover service endpoint for the social networking library + String currentScope = ScopeProvider.instance.get(); + String tokenUser = SecurityTokenProvider.instance.get(); + + logger.info("Current scope for writeProductPost is " + currentScope + " and token is " + tokenUser.substring(0, 10) + "***************"); + String basePath = new ServiceEndPointReaderSocial(currentScope).getBasePath(); + + if(basePath == null){ + + logger.error("Unable to write a post because there is no social networking service available"); + + }else{ + + try(CloseableHttpClient client = HttpClientBuilder.create().build();){ + + // ask token application + HttpPost postRequest = new HttpPost(basePath + SOCIAL_SERVICE_APPLICATION_TOKEN + "?gcube-token=" + tokenUser); + StringEntity input = new StringEntity("{\"app_id\":\"" + APPLICATION_ID_CATALOGUE_MANAGER + "\"}"); + input.setContentType(MEDIATYPE_JSON); + postRequest.setEntity(input); + HttpResponse response = client.execute(postRequest); + + logger.debug("Url is " + basePath + SOCIAL_SERVICE_APPLICATION_TOKEN + "?gcube-token=" + tokenUser); + + if (response.getStatusLine().getStatusCode() != 201) { + throw new RuntimeException("Failed to retrieve application token : HTTP error code : " + + response.getStatusLine().getStatusCode()); + }else{ + + Map mapResponseGeneratedToken = getResponseEntityAsJSON(response); + boolean successGeneratedToken = (boolean)mapResponseGeneratedToken.get("success"); + if(!successGeneratedToken){ + + throw new RuntimeException("Failed to generate the token for the application!" + + " Error message is " + mapResponseGeneratedToken.get("message")); + + }else{ + + String applicationToken = (String)mapResponseGeneratedToken.get("result"); + + // replace + String message = NOTIFICATION_MESSAGE.replace("PRODUCT_TITLE", productName).replace("PRODUCT_URL", productUrl).replace("USER_FULLNAME", userFullname); + + if(hashtags != null && !hashtags.isEmpty()) + for (String hashtag : hashtags) { + String modifiedHashtag = hashtag.replaceAll(" ", "_").replace("_+", "_"); + if(modifiedHashtag.endsWith("_")) + modifiedHashtag = modifiedHashtag.substring(0, modifiedHashtag.length() - 1); + message += " #" + modifiedHashtag; // ckan accepts tag with empty spaces, we don't + } + + logger.info("The post that is going to be written is -> " + message); + postRequest = new HttpPost(basePath + SOCIAL_SERVICE_WRITE_APPLICATION_POST + "?gcube-token=" + applicationToken); + input = new StringEntity("{\"text\":\"" + message + "\", \"enable_notification\" : "+ enablePostNotification+ "}"); + input.setContentType(MEDIATYPE_JSON); + postRequest.setEntity(input); + response = client.execute(postRequest); + + Map mapResponseWritePost = getResponseEntityAsJSON(response); + + if (response.getStatusLine().getStatusCode() != 201) + throw new RuntimeException("Failed to write application post : HTTP error code : " + + response.getStatusLine().getStatusCode() + mapResponseWritePost.get("message")); + } + + } + + }catch(Exception e){ + logger.error("Failed to create a post", e); + } + } + } + + /** + * Convert the json response to a map + * @param response + * @return + */ + public static Map getResponseEntityAsJSON(HttpResponse response){ + + Map toReturn = null; + HttpEntity entity = response.getEntity(); + + if (entity != null) { + try { + toReturn = new HashMap(); + InputStream is = entity.getContent(); + BufferedReader reader = new BufferedReader(new InputStreamReader(is)); + StringBuilder sb = new StringBuilder(); + + String line = null; + + while ((line = reader.readLine()) != null) { + sb.append(line + "\n"); + } + + logger.debug("Response as string is " + sb.toString()); + ObjectMapper objectMapper = new ObjectMapper(); + toReturn = objectMapper.readValue(sb.toString(), HashMap.class); + logger.debug("Map is " + toReturn); + + }catch(Exception e){ + logger.error("Failed to read json object", e); + } + } + + return toReturn; + } +} \ No newline at end of file diff --git a/src/test/java/org/gcube/portlets/widgets/ckandatapublisherwidget/client/TestClass.java b/src/test/java/org/gcube/portlets/widgets/ckandatapublisherwidget/client/TestClass.java index 67ffefc..050699c 100644 --- a/src/test/java/org/gcube/portlets/widgets/ckandatapublisherwidget/client/TestClass.java +++ b/src/test/java/org/gcube/portlets/widgets/ckandatapublisherwidget/client/TestClass.java @@ -1,8 +1,10 @@ package org.gcube.portlets.widgets.ckandatapublisherwidget.client; import java.util.ArrayList; +import java.util.Arrays; import java.util.List; +import org.gcube.common.authorization.library.provider.SecurityTokenProvider; import org.gcube.common.homelibrary.home.HomeLibrary; import org.gcube.common.homelibrary.home.exceptions.HomeNotFoundException; import org.gcube.common.homelibrary.home.exceptions.InternalErrorException; @@ -21,6 +23,7 @@ import org.gcube.datacatalogue.metadatadiscovery.bean.jaxb.MetadataFormat; import org.gcube.datacatalogue.metadatadiscovery.bean.jaxb.MetadataValidator; import org.gcube.datacatalogue.metadatadiscovery.bean.jaxb.MetadataVocabulary; import org.gcube.portlets.widgets.ckandatapublisherwidget.server.CKANPublisherServicesImpl; +import org.gcube.portlets.widgets.ckandatapublisherwidget.server.utils.Utils; import org.gcube.portlets.widgets.ckandatapublisherwidget.shared.DataType; import org.gcube.portlets.widgets.ckandatapublisherwidget.shared.MetaDataTypeWrapper; import org.gcube.portlets.widgets.ckandatapublisherwidget.shared.MetadataFieldWrapper; @@ -163,4 +166,18 @@ public class TestClass { } + //@Test + public void testDataCatalogueManagerApplication(){ + + String token = "d423aed7-e9e2-424a-b9e7-2bbbd151d9c4-98187548"; + String scope = "/gcube/devNext/NextNext"; + + ScopeProvider.instance.set(scope); + SecurityTokenProvider.instance.set(token); + + Utils.writeProductPost("a great test product", + "https://next.d4science.org/group/nextnext/data-catalogue?path=/dataset/test_for_visibility", + "Costantino Perciante", Arrays.asList("tag1", "tag2", "tag33"), false); + + } }