porting to catalogue-util-library should be completed

This commit is contained in:
Francesco Mangiacrapa 2021-02-11 16:13:23 +01:00
parent 1395dd63eb
commit 37254e0096
8 changed files with 563 additions and 550 deletions

View File

@ -0,0 +1,7 @@
<root>
<facet id="jst.jaxrs">
<node name="libprov">
<attribute name="provider-id" value="jaxrs-no-op-library-provider"/>
</node>
</facet>
</root>

View File

@ -4,4 +4,5 @@
<installed facet="jst.web" version="2.3"/> <installed facet="jst.web" version="2.3"/>
<installed facet="wst.jsdt.web" version="1.0"/> <installed facet="wst.jsdt.web" version="1.0"/>
<installed facet="java" version="1.8"/> <installed facet="java" version="1.8"/>
<installed facet="jst.jaxrs" version="2.0"/>
</faceted-project> </faceted-project>

View File

@ -5,6 +5,13 @@ All notable changes to this project will be documented in this file.
This project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). This project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
## [v2.0.0-SNAPSHOT] - 2021-02-11
**Enhancements**
[#19764] Porting ckan-metadata-publisher-widget to catalogue-util-library
## [v1.6.2] - 2021-02-08 ## [v1.6.2] - 2021-02-08
**Bug Fixes** **Bug Fixes**

31
pom.xml
View File

@ -146,28 +146,27 @@
<scope>provided</scope> <scope>provided</scope>
</dependency> </dependency>
<!-- <dependency> --> <!-- <dependency> -->
<!-- <groupId>org.gcube.data-catalogue</groupId> --> <!-- <groupId>org.gcube.data-catalogue</groupId> -->
<!-- <artifactId>ckan-util-library</artifactId> --> <!-- <artifactId>ckan-util-library</artifactId> -->
<!-- <version>[2.0.0, 3.0.0-SNAPSHOT)</version> --> <!-- <version>[2.0.0, 3.0.0-SNAPSHOT)</version> -->
<!-- <scope>compile</scope> --> <!-- <scope>compile</scope> -->
<!-- <exclusions> --> <!-- <exclusions> -->
<!-- <exclusion> --> <!-- <exclusion> -->
<!-- <groupId>org.gcube.data-catalogue</groupId> --> <!-- <groupId>org.gcube.data-catalogue</groupId> -->
<!-- <artifactId>gcubedatacatalogue-metadata-discovery</artifactId> --> <!-- <artifactId>gcubedatacatalogue-metadata-discovery</artifactId> -->
<!-- </exclusion> --> <!-- </exclusion> -->
<!-- </exclusions> --> <!-- </exclusions> -->
<!-- </dependency> --> <!-- </dependency> -->
<dependency> <dependency>
<groupId>org.gcube.datacatalogue</groupId> <groupId>org.gcube.datacatalogue</groupId>
<artifactId>catalogue-util-library</artifactId> <artifactId>catalogue-util-library</artifactId>
<version>0.1.0-SNAPSHOT</version> <version>[0.1.0-SNAPSHOT, 1.0.0)</version>
<scope>system</scope> <scope>compile</scope>
<systemPath>/home/francesco/git/catalogue-util-library/target/catalogue-util-library-0.1.0-SNAPSHOT.jar</systemPath>
</dependency> </dependency>
<dependency> <dependency>
<groupId>org.gcube.core</groupId> <groupId>org.gcube.core</groupId>
<artifactId>common-scope-maps</artifactId> <artifactId>common-scope-maps</artifactId>

View File

@ -5,7 +5,6 @@ import java.util.Arrays;
import java.util.Calendar; import java.util.Calendar;
import java.util.Collections; import java.util.Collections;
import java.util.Comparator; import java.util.Comparator;
import java.util.HashMap;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.Map.Entry; import java.util.Map.Entry;
@ -20,9 +19,9 @@ import org.gcube.datacatalogue.ckanutillibrary.server.utils.CatalogueUtilMethods
import org.gcube.datacatalogue.ckanutillibrary.server.utils.SessionCatalogueAttributes; import org.gcube.datacatalogue.ckanutillibrary.server.utils.SessionCatalogueAttributes;
import org.gcube.datacatalogue.ckanutillibrary.shared.ResourceBean; import org.gcube.datacatalogue.ckanutillibrary.shared.ResourceBean;
import org.gcube.datacatalogue.ckanutillibrary.shared.RolesCkanGroupOrOrg; import org.gcube.datacatalogue.ckanutillibrary.shared.RolesCkanGroupOrOrg;
import org.gcube.datacatalogue.ckanutillibrary.shared.jackan.model.CkanGroup;
import org.gcube.datacatalogue.ckanutillibrary.shared.jackan.model.CkanLicense;
import org.gcube.portlets.widgets.ckandatapublisherwidget.client.CKanPublisherService; import org.gcube.portlets.widgets.ckandatapublisherwidget.client.CKanPublisherService;
import org.gcube.portlets.widgets.ckandatapublisherwidget.server.threads.AssociationToGroupAndNotifyThread;
import org.gcube.portlets.widgets.ckandatapublisherwidget.server.threads.WritePostCatalogueManagerThread;
import org.gcube.portlets.widgets.ckandatapublisherwidget.server.utils.CatalogueRoleManager; import org.gcube.portlets.widgets.ckandatapublisherwidget.server.utils.CatalogueRoleManager;
import org.gcube.portlets.widgets.ckandatapublisherwidget.server.utils.DiscoverTagsList; import org.gcube.portlets.widgets.ckandatapublisherwidget.server.utils.DiscoverTagsList;
import org.gcube.portlets.widgets.ckandatapublisherwidget.server.utils.GenericUtils; import org.gcube.portlets.widgets.ckandatapublisherwidget.server.utils.GenericUtils;
@ -48,7 +47,7 @@ import com.liferay.portal.kernel.log.Log;
import com.liferay.portal.kernel.log.LogFactoryUtil; import com.liferay.portal.kernel.log.LogFactoryUtil;
import com.liferay.portal.service.UserLocalServiceUtil; import com.liferay.portal.service.UserLocalServiceUtil;
import eu.trentorise.opendata.jackan.model.CkanLicense;
/** /**
@ -328,8 +327,8 @@ public class CKANPublisherServicesImpl extends RemoteServiceServlet implements C
logger.debug("The user wants to publish in organization with name " + organizationNameOrId); logger.debug("The user wants to publish in organization with name " + organizationNameOrId);
String scope = getScopeFromOrgName(organizationNameOrId); String scope = getScopeFromOrgName(organizationNameOrId);
DataCatalogue utils = getCatalogue(scope); DataCatalogue utils = getCatalogue(scope);
String datasetId = utils.createCKanDatasetMultipleCustomFields(userName, String datasetId = utils.createCkanDatasetMultipleCustomFields(userName,
title, title,
null, null,
organizationNameOrId, organizationNameOrId,
@ -344,6 +343,7 @@ public class CKANPublisherServicesImpl extends RemoteServiceServlet implements C
customFields, customFields,
resources, resources,
setPublic, setPublic,
true,
true); true);
/** /**
@ -374,41 +374,43 @@ public class CKANPublisherServicesImpl extends RemoteServiceServlet implements C
utils.patchProductCustomFields(datasetId, userApiKey, addField, false); utils.patchProductCustomFields(datasetId, userApiKey, addField, false);
*/ */
//TODO SHOULD BE NOTIFIED by gCAT
// start a thread that will associate this dataset with the group // start a thread that will associate this dataset with the group
if(/*toCreate.getChosenType() != null ||*/ toCreate.getGroups() != null){ // if(/*toCreate.getChosenType() != null ||*/ toCreate.getGroups() != null){
//
AssociationToGroupAndNotifyThread threadAssociationToGroup = // AssociationToGroupAndNotifyThread threadAssociationToGroup =
new AssociationToGroupAndNotifyThread( // new AssociationToGroupAndNotifyThread(
toCreate.getGroups(), // toCreate.getGroups(),
toCreate.getGroupsForceCreation(), // toCreate.getGroupsForceCreation(),
null, //toCreate.getChosenType(), TODO // null, //toCreate.getChosenType(), TODO
datasetUrl, // datasetUrl,
datasetId, // datasetId,
toCreate.getTitle(), // toCreate.getTitle(),
GenericUtils.getCurrentUser(getThreadLocalRequest()).getFullname(), // GenericUtils.getCurrentUser(getThreadLocalRequest()).getFullname(),
userName, // userName,
utils, // utils,
organizationNameOrId, // organizationNameOrId,
getThreadLocalRequest() // getThreadLocalRequest()
); // );
threadAssociationToGroup.start(); // threadAssociationToGroup.start();
//
} // }
//
// launch notification thread // // launch notification thread
WritePostCatalogueManagerThread threadWritePost = // WritePostCatalogueManagerThread threadWritePost =
new WritePostCatalogueManagerThread( // new WritePostCatalogueManagerThread(
userName, // userName,
scope, // scope,
toCreate.getTitle(), // toCreate.getTitle(),
datasetUrl, // datasetUrl,
false, // send notification to other people // false, // send notification to other people
toCreate.getTags(), // toCreate.getTags(),
GenericUtils.getCurrentUser(getThreadLocalRequest()).getFullname(), // GenericUtils.getCurrentUser(getThreadLocalRequest()).getFullname(),
GenericUtils.getCurrentClientUrl(getThreadLocalRequest()) // GenericUtils.getCurrentClientUrl(getThreadLocalRequest())
); // );
threadWritePost.start(); // threadWritePost.start();
return toCreate; return toCreate;
}else{ }else{
@ -450,7 +452,7 @@ public class CKANPublisherServicesImpl extends RemoteServiceServlet implements C
// get the scope in which we should discover the ckan instance given the organization name in which the dataset was created // get the scope in which we should discover the ckan instance given the organization name in which the dataset was created
String scope = getScopeFromOrgName(resource.getOrganizationNameDatasetParent()); String scope = getScopeFromOrgName(resource.getOrganizationNameDatasetParent());
DataCatalogue catalogue = getCatalogue(scope); DataCatalogue catalogue = getCatalogue(scope);
String resourceId = catalogue.addResourceToDataset(resourceBean, catalogue.getApiKeyFromUsername(username)); String resourceId = catalogue.addResourceToDataset(resourceBean);
if(resourceId != null){ if(resourceId != null){
logger.debug("Resource " + resource.getName() + " is now available"); logger.debug("Resource " + resource.getName() + " is now available");
@ -475,13 +477,11 @@ public class CKANPublisherServicesImpl extends RemoteServiceServlet implements C
return deleted; return deleted;
}else{ }else{
String username = GenericUtils.getCurrentUser(getThreadLocalRequest()).getUsername();
try{ try{
// get the scope in which we should discover the ckan instance given the organization name in which the dataset was created // get the scope in which we should discover the ckan instance given the organization name in which the dataset was created
String scope = getScopeFromOrgName(resource.getOrganizationNameDatasetParent()); String scope = getScopeFromOrgName(resource.getOrganizationNameDatasetParent());
DataCatalogue catalogue = getCatalogue(scope); DataCatalogue catalogue = getCatalogue(scope);
deleted = catalogue. deleted = catalogue.deleteResourceFromDataset(resource.getOriginalIdInWorkspace());
deleteResourceFromDataset(resource.getOriginalIdInWorkspace(), catalogue.getApiKeyFromUsername(username));
if(deleted){ if(deleted){
logger.info("Resource described by " + resource + " deleted"); logger.info("Resource described by " + resource + " deleted");
}else }else
@ -565,7 +565,10 @@ public class CKANPublisherServicesImpl extends RemoteServiceServlet implements C
List<OrganizationBean> toReturn = new ArrayList<OrganizationBean>(); List<OrganizationBean> toReturn = new ArrayList<OrganizationBean>();
if(isWithinPortal()){ if(isWithinPortal()){
String username = GenericUtils.getCurrentUser(getThreadLocalRequest()).getUsername(); GCubeUser user = GenericUtils.getCurrentUser(getThreadLocalRequest());
String username = null;
if(user!=null)
username = user.getUsername();
logger.debug("Request for user " + username + " groups. Organization name is " + orgName); logger.debug("Request for user " + username + " groups. Organization name is " + orgName);
@ -584,10 +587,9 @@ public class CKANPublisherServicesImpl extends RemoteServiceServlet implements C
//Fixing Incident #12563 //Fixing Incident #12563
try{ try{
DataCatalogue catalogue = getCatalogue(scope); DataCatalogue catalogue = getCatalogue(scope);
String apiKey = catalogue.getApiKeyFromUsername(username);
//Fixing Incident #12563 //Fixing Incident #12563
if(apiKey!=null && !apiKey.isEmpty()){ if(username!=null && !username.isEmpty()){
Map<String, Map<CkanGroup, RolesCkanGroupOrOrg>> mapRoleGroup = catalogue.getUserRoleByGroup(username, apiKey); Map<String, Map<CkanGroup, RolesCkanGroupOrOrg>> mapRoleGroup = catalogue.getUserRoleByGroup(username);
Set<Entry<String, Map<CkanGroup, RolesCkanGroupOrOrg>>> set = mapRoleGroup.entrySet(); Set<Entry<String, Map<CkanGroup, RolesCkanGroupOrOrg>>> set = mapRoleGroup.entrySet();
for (Entry<String, Map<CkanGroup, RolesCkanGroupOrOrg>> entry : set) { for (Entry<String, Map<CkanGroup, RolesCkanGroupOrOrg>> entry : set) {
Set<Entry<CkanGroup, RolesCkanGroupOrOrg>> subSet = entry.getValue().entrySet(); Set<Entry<CkanGroup, RolesCkanGroupOrOrg>> subSet = entry.getValue().entrySet();

View File

@ -1,160 +1,161 @@
package org.gcube.portlets.widgets.ckandatapublisherwidget.server.threads; //package org.gcube.portlets.widgets.ckandatapublisherwidget.server.threads;
//
import java.util.ArrayList; //import java.util.ArrayList;
import java.util.Iterator; //import java.util.Iterator;
import java.util.List; //import java.util.List;
import java.util.Map; //import java.util.Map;
//
import javax.servlet.http.HttpServletRequest; //import javax.servlet.http.HttpServletRequest;
//
import org.gcube.common.portal.mailing.EmailNotification; //import org.gcube.common.portal.mailing.EmailNotification;
import org.gcube.datacatalogue.ckanutillibrary.server.DataCatalogue; //import org.gcube.datacatalogue.ckanutillibrary.server.DataCatalogue;
import org.gcube.datacatalogue.ckanutillibrary.server.utils.CatalogueUtilMethods; //import org.gcube.datacatalogue.ckanutillibrary.server.utils.CatalogueUtilMethods;
import org.gcube.datacatalogue.ckanutillibrary.shared.RolesCkanGroupOrOrg; //import org.gcube.datacatalogue.ckanutillibrary.shared.RolesCkanGroupOrOrg;
import org.gcube.portlets.widgets.ckandatapublisherwidget.shared.OrganizationBean; //import org.gcube.datacatalogue.ckanutillibrary.shared.jackan.model.CkanGroup;
import org.gcube.vomanagement.usermanagement.UserManager; //import org.gcube.portlets.widgets.ckandatapublisherwidget.shared.OrganizationBean;
import org.gcube.vomanagement.usermanagement.impl.LiferayUserManager; //import org.gcube.vomanagement.usermanagement.UserManager;
//import org.gcube.vomanagement.usermanagement.impl.LiferayUserManager;
import com.liferay.portal.kernel.log.Log; //
import com.liferay.portal.kernel.log.LogFactoryUtil; //import com.liferay.portal.kernel.log.Log;
//import com.liferay.portal.kernel.log.LogFactoryUtil;
import eu.trentorise.opendata.jackan.model.CkanGroup; //
//
/** //
* Associate the dataset to a group and send notifications to group's admins. ///**
* @author Costantino Perciante at ISTI-CNR (costantino.perciante@isti.cnr.it) // * Associate the dataset to a group and send notifications to group's admins.
*/ // * @author Costantino Perciante at ISTI-CNR (costantino.perciante@isti.cnr.it)
public class AssociationToGroupAndNotifyThread extends Thread { // */
//public class AssociationToGroupAndNotifyThread extends Thread {
//private static final Logger logger = LoggerFactory.getLogger(AssociationToGroupAndNotifyThread.class); //
private static final Log logger = LogFactoryUtil.getLog(AssociationToGroupAndNotifyThread.class); // //private static final Logger logger = LoggerFactory.getLogger(AssociationToGroupAndNotifyThread.class);
private static final String PRODUCT_ASSOCIATED_TO_GROUP_SUBJECT = "Item $TITLE added to group $GROUP"; // private static final Log logger = LogFactoryUtil.getLog(AssociationToGroupAndNotifyThread.class);
private static final String PRODUCT_ASSOCIATED_TO_GROUP_BODY = "Dear user,<br> a new item named '<b>$TITLE</b>' has been " // private static final String PRODUCT_ASSOCIATED_TO_GROUP_SUBJECT = "Item $TITLE added to group $GROUP";
+ "just published by $USER_FULLNAME in <b>$GROUP</b> .<br>" // private static final String PRODUCT_ASSOCIATED_TO_GROUP_BODY = "Dear user,<br> a new item named '<b>$TITLE</b>' has been "
+ "You can find it here $DATASET_URL"; // + "just published by $USER_FULLNAME in <b>$GROUP</b> .<br>"
// + "You can find it here $DATASET_URL";
private String groupTitle; //
private String datasetId; // private String groupTitle;
private String username; // private String datasetId;
private String datasetTitle; // private String username;
private String userFullName; // private String datasetTitle;
private DataCatalogue catalogue; // private String userFullName;
// private String organization; // private DataCatalogue catalogue;
private List<OrganizationBean> groups; // // private String organization;
private HttpServletRequest request; // private List<OrganizationBean> groups;
private String datasetUrl; // private HttpServletRequest request;
private List<OrganizationBean> groupsForceCreation; // private String datasetUrl;
// private List<OrganizationBean> groupsForceCreation;
/** //
* @param list // /**
* @param groupTitle // * @param list
* @param datasetId // * @param groupTitle
* @param username // * @param datasetId
* @param catalogue // * @param username
*/ // * @param catalogue
public AssociationToGroupAndNotifyThread(List<OrganizationBean> groups, List<OrganizationBean> groupsForceCreation, String groupTitle, String datasetUrl, String datasetId, String datasetTitle, String userFullName, // */
String username, DataCatalogue catalogue, String organization, HttpServletRequest request) { // public AssociationToGroupAndNotifyThread(List<OrganizationBean> groups, List<OrganizationBean> groupsForceCreation, String groupTitle, String datasetUrl, String datasetId, String datasetTitle, String userFullName,
this.request = request; // String username, DataCatalogue catalogue, String organization, HttpServletRequest request) {
this.groups = groups == null ? new ArrayList<OrganizationBean>() : groups; // this.request = request;
this.groupsForceCreation = groupsForceCreation; // this.groups = groups == null ? new ArrayList<OrganizationBean>() : groups;
this.groupTitle = groupTitle; // this.groupsForceCreation = groupsForceCreation;
this.datasetId = datasetId; // this.groupTitle = groupTitle;
this.username = username; // this.datasetId = datasetId;
this.catalogue = catalogue; // this.username = username;
// this.organization = organization; // this.catalogue = catalogue;
this.datasetTitle = datasetTitle; // // this.organization = organization;
this.userFullName = userFullName; // this.datasetTitle = datasetTitle;
this.datasetUrl = datasetUrl; // this.userFullName = userFullName;
} // this.datasetUrl = datasetUrl;
// }
@Override //
public void run() { // @Override
// public void run() {
logger.info("Association thread started to put the dataset with id = "+ datasetId + " into group with title " + groupTitle + " for user " + username); //
// logger.info("Association thread started to put the dataset with id = "+ datasetId + " into group with title " + groupTitle + " for user " + username);
// force creation of groups if needed //
if(groupsForceCreation != null){ // // force creation of groups if needed
logger.info("Groups that must be created before association are " + groupsForceCreation); // if(groupsForceCreation != null){
for (OrganizationBean groupToForce : groupsForceCreation) { // logger.info("Groups that must be created before association are " + groupsForceCreation);
try{ // for (OrganizationBean groupToForce : groupsForceCreation) {
CkanGroup group = catalogue.createGroup(groupToForce.getName(), groupToForce.getTitle(), ""); // try{
if(group == null) // CkanGroup group = catalogue.createGroup(groupToForce.getName(), groupToForce.getTitle(), "");
logger.error("Unable to retrieve or create group with name " + groupToForce); // if(group == null)
else // logger.error("Unable to retrieve or create group with name " + groupToForce);
groups.add(new OrganizationBean(group.getTitle(), group.getName(), false, groupToForce.isPropagateUp())); // else
}catch(Exception e){ // groups.add(new OrganizationBean(group.getTitle(), group.getName(), false, groupToForce.isPropagateUp()));
logger.error("Failed to check if a group with this info " + groupToForce + " already exists or can be created"); // }catch(Exception e){
} // logger.error("Failed to check if a group with this info " + groupToForce + " already exists or can be created");
} // }
// }
} //
// }
logger.info("Other groups to which the product should be associate are " + groups); //
// logger.info("Other groups to which the product should be associate are " + groups);
if(groups != null) //
for (OrganizationBean groupBean : groups) { // if(groups != null)
boolean putIntoGroup = catalogue.assignDatasetToGroup(groupBean.getName(), datasetId, catalogue.getApiKeyFromUsername(username), groupBean.isPropagateUp()); // for (OrganizationBean groupBean : groups) {
logger.info("Was product put into group" + groupBean.getTitle() + "? " + putIntoGroup); // boolean putIntoGroup = catalogue.assignDatasetToGroup(groupBean.getName(), datasetId, groupBean.isPropagateUp());
if(putIntoGroup) // logger.info("Was product put into group" + groupBean.getTitle() + "? " + putIntoGroup);
notifyGroupAdmins(catalogue, groupBean.getName() ,groupBean.getTitle(), username); // if(putIntoGroup)
} // notifyGroupAdmins(catalogue, groupBean.getName() ,groupBean.getTitle(), username);
// }
} //
// }
/** //
* Send a notification to the group admin(s) about the just added product // /**
* @param username // * Send a notification to the group admin(s) about the just added product
* @param groupTitle // * @param username
* @param catalogue // * @param groupTitle
*/ // * @param catalogue
private void notifyGroupAdmins(DataCatalogue catalogue, String groupName, String groupTitle, String username){ // */
// private void notifyGroupAdmins(DataCatalogue catalogue, String groupName, String groupTitle, String username){
// get the groups admin //
Map<RolesCkanGroupOrOrg, List<String>> userAndRoles = catalogue.getRolesAndUsersGroup(groupName); // // get the groups admin
// Map<RolesCkanGroupOrOrg, List<String>> userAndRoles = catalogue.getRolesAndUsersGroup(groupName);
if(userAndRoles.containsKey(RolesCkanGroupOrOrg.ADMIN)){ //
// if(userAndRoles.containsKey(RolesCkanGroupOrOrg.ADMIN)){
List<String> admins = userAndRoles.get(RolesCkanGroupOrOrg.ADMIN); //
List<String> adminsEmails = new ArrayList<String>(); // List<String> admins = userAndRoles.get(RolesCkanGroupOrOrg.ADMIN);
// List<String> adminsEmails = new ArrayList<String>();
for(int i = 0; i < admins.size(); i++){ //
String convertedName = CatalogueUtilMethods.fromCKanUsernameToUsername(admins.get(i)); // for(int i = 0; i < admins.size(); i++){
admins.set(i, convertedName); // String convertedName = CatalogueUtilMethods.fromCKanUsernameToUsername(admins.get(i));
} // admins.set(i, convertedName);
// }
// remove the same user who published the product if he/she is an admin of the group //
int indexOfUser = admins.indexOf(username); // // remove the same user who published the product if he/she is an admin of the group
if(indexOfUser >= 0) // int indexOfUser = admins.indexOf(username);
admins.remove(indexOfUser); // if(indexOfUser >= 0)
// admins.remove(indexOfUser);
// further cleaning of the list (for users that are only in ckan... sysadmin for example) //
UserManager um = new LiferayUserManager(); // // further cleaning of the list (for users that are only in ckan... sysadmin for example)
Iterator<String> adminIt = admins.iterator(); // UserManager um = new LiferayUserManager();
// Iterator<String> adminIt = admins.iterator();
while (adminIt.hasNext()) { //
String admin = (String) adminIt.next(); // while (adminIt.hasNext()) {
try{ // String admin = (String) adminIt.next();
adminsEmails.add(um.getUserByUsername(admin).getEmail()); // try{
}catch(Exception e){ // adminsEmails.add(um.getUserByUsername(admin).getEmail());
logger.error("User with username " + admin + " doesn't exist in Liferay"); // }catch(Exception e){
adminIt.remove(); // logger.error("User with username " + admin + " doesn't exist in Liferay");
} // adminIt.remove();
} // }
// }
logger.info("The list of admins for group " + groupTitle + " is " + admins); //
// logger.info("The list of admins for group " + groupTitle + " is " + admins);
if(admins.isEmpty()) //
return; // if(admins.isEmpty())
// return;
// send the email //
EmailNotification mailToSend = new EmailNotification( // // send the email
adminsEmails, // EmailNotification mailToSend = new EmailNotification(
PRODUCT_ASSOCIATED_TO_GROUP_SUBJECT.replace("$TITLE", datasetTitle).replace("$GROUP", groupTitle), // adminsEmails,
PRODUCT_ASSOCIATED_TO_GROUP_BODY.replace("$TITLE", datasetTitle).replace("$GROUP", groupTitle).replace("$USER_FULLNAME", userFullName).replace("$DATASET_URL", datasetUrl), // PRODUCT_ASSOCIATED_TO_GROUP_SUBJECT.replace("$TITLE", datasetTitle).replace("$GROUP", groupTitle),
request); // PRODUCT_ASSOCIATED_TO_GROUP_BODY.replace("$TITLE", datasetTitle).replace("$GROUP", groupTitle).replace("$USER_FULLNAME", userFullName).replace("$DATASET_URL", datasetUrl),
mailToSend.sendEmail(); // request);
// mailToSend.sendEmail();
}else //
logger.warn("It seems there is no user with role Admin in group " + groupTitle); // }else
} // logger.warn("It seems there is no user with role Admin in group " + groupTitle);
} // }
//}

View File

@ -1,325 +1,322 @@
package org.gcube.portlets.widgets.ckandatapublisherwidget.server.threads; //package org.gcube.portlets.widgets.ckandatapublisherwidget.server.threads;
//
import java.io.IOException; //import java.io.IOException;
import java.net.HttpURLConnection; //import java.net.HttpURLConnection;
import java.util.List; //import java.util.List;
//
import org.gcube.common.authorization.library.provider.SecurityTokenProvider; //import org.apache.http.HttpEntity;
import org.gcube.common.scope.api.ScopeProvider; //import org.apache.http.HttpResponse;
import org.gcube.portlets.widgets.ckandatapublisherwidget.server.utils.GCoreEndPointReaderSocial; //import org.apache.http.client.ClientProtocolException;
import org.gcube.portlets.widgets.ckandatapublisherwidget.server.utils.GenericUtils; //import org.apache.http.client.methods.HttpPost;
import org.json.simple.JSONObject; //import org.apache.http.entity.StringEntity;
import org.json.simple.parser.JSONParser; //import org.apache.http.impl.client.CloseableHttpClient;
//import org.apache.http.impl.client.HttpClientBuilder;
import com.liferay.portal.kernel.log.Log; //import org.apache.http.util.EntityUtils;
import com.liferay.portal.kernel.log.LogFactoryUtil; //import org.gcube.common.authorization.library.provider.SecurityTokenProvider;
//import org.gcube.common.scope.api.ScopeProvider;
import eu.trentorise.opendata.jackan.internal.org.apache.http.Header; //import org.gcube.portlets.widgets.ckandatapublisherwidget.server.utils.GCoreEndPointReaderSocial;
import eu.trentorise.opendata.jackan.internal.org.apache.http.HttpEntity; //import org.gcube.portlets.widgets.ckandatapublisherwidget.server.utils.GenericUtils;
import eu.trentorise.opendata.jackan.internal.org.apache.http.HttpResponse; //
import eu.trentorise.opendata.jackan.internal.org.apache.http.client.ClientProtocolException; //import com.google.gwt.json.client.JSONParser;
import eu.trentorise.opendata.jackan.internal.org.apache.http.client.methods.HttpPost; //import com.liferay.portal.kernel.log.Log;
import eu.trentorise.opendata.jackan.internal.org.apache.http.entity.StringEntity; //import com.liferay.portal.kernel.log.LogFactoryUtil;
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.internal.org.apache.http.util.EntityUtils; ///**
// * Let the Product Catalogue Manager write a post in a VRE and alert there is a new product
// * @author Costantino Perciante at ISTI-CNR
/** // * (costantino.perciante@isti.cnr.it)
* Let the Product Catalogue Manager write a post in a VRE and alert there is a new product // */
* @author Costantino Perciante at ISTI-CNR //public class WritePostCatalogueManagerThread extends Thread {
* (costantino.perciante@isti.cnr.it) //
*/ // public static final String APPLICATION_ID_CATALOGUE_MANAGER = "org.gcube.datacatalogue.ProductCatalogue";
public class WritePostCatalogueManagerThread extends Thread { // private static final String NOTIFICATION_MESSAGE = "Dear members,\n$USER_FULLNAME just published the item '$PRODUCT_TITLE'.\nYou can find it at: $PRODUCT_URL\n";
// private static final String SOCIAL_SERVICE_APPLICATION_TOKEN = "2/tokens/generate-application-token";
public static final String APPLICATION_ID_CATALOGUE_MANAGER = "org.gcube.datacatalogue.ProductCatalogue"; // private static final String SOCIAL_SERVICE_WRITE_APPLICATION_POST = "2/posts/write-post-app";
private static final String NOTIFICATION_MESSAGE = "Dear members,\n$USER_FULLNAME just published the item '$PRODUCT_TITLE'.\nYou can find it at: $PRODUCT_URL\n"; // private static final String MEDIATYPE_JSON = "application/json";
private static final String SOCIAL_SERVICE_APPLICATION_TOKEN = "2/tokens/generate-application-token"; // private static final Log logger = LogFactoryUtil.getLog(WritePostCatalogueManagerThread.class);
private static final String SOCIAL_SERVICE_WRITE_APPLICATION_POST = "2/posts/write-post-app"; // private String username;
private static final String MEDIATYPE_JSON = "application/json"; // private String scope;
private static final Log logger = LogFactoryUtil.getLog(WritePostCatalogueManagerThread.class); // private String productTitle;
private String username; // private String productUrl;
private String scope; // private boolean enableNotification;
private String productTitle; // private List<String> hashtags;
private String productUrl; // private String userFullName;
private boolean enableNotification; // private String userCurrentUrl;
private List<String> hashtags; //
private String userFullName; // /**
private String userCurrentUrl; // * @param token
// * @param scope
/** // * @param productTitle
* @param token // * @param productUrl
* @param scope // * @param enableNotification
* @param productTitle // * @param hashtags
* @param productUrl // * @param userFullName
* @param enableNotification // */
* @param hashtags // public WritePostCatalogueManagerThread(
* @param userFullName // String username, String scope,
*/ // String productTitle, String productUrl, boolean enableNotification,
public WritePostCatalogueManagerThread( // List<String> hashtags, String userFullName, String userCurrentUrl) {
String username, String scope, // super();
String productTitle, String productUrl, boolean enableNotification, // this.username = username;
List<String> hashtags, String userFullName, String userCurrentUrl) { // this.scope = scope;
super(); // this.productTitle = productTitle;
this.username = username; // this.productUrl = productUrl;
this.scope = scope; // this.enableNotification = enableNotification;
this.productTitle = productTitle; // this.hashtags = hashtags;
this.productUrl = productUrl; // this.userFullName = userFullName;
this.enableNotification = enableNotification; // this.userCurrentUrl = userCurrentUrl;
this.hashtags = hashtags; // }
this.userFullName = userFullName; //
this.userCurrentUrl = userCurrentUrl; // @Override
} // public void run() {
//
@Override // try{
public void run() { // // evaluate user's token for this scope
// String token = GenericUtils.tryGetElseCreateToken(username, scope);
try{ //
// evaluate user's token for this scope // if(token == null){
String token = GenericUtils.tryGetElseCreateToken(username, scope); // logger.warn("Unable to proceed, user's token is not available");
// return;
if(token == null){ // }
logger.warn("Unable to proceed, user's token is not available"); //
return; // logger.info("Started request to write application post "
} // + "for new product created. Scope is " + scope + " and "
// + "token is " + token.substring(0, 10) + "****************");
logger.info("Started request to write application post " //
+ "for new product created. Scope is " + scope + " and " // // set token and scope
+ "token is " + token.substring(0, 10) + "****************"); // ScopeProvider.instance.set(scope);
// SecurityTokenProvider.instance.set(token);
// set token and scope //
ScopeProvider.instance.set(scope); // //see Feature #17577
SecurityTokenProvider.instance.set(token); // /*final String profilePageURL = GCubePortalConstants.PREFIX_GROUP_URL + extractOrgFriendlyURL(userCurrentUrl) + GCubePortalConstants.USER_PROFILE_FRIENDLY_URL;
//
//see Feature #17577 // userFullName = "<a class=\"link\" href=\"" + profilePageURL + "?"+
/*final String profilePageURL = GCubePortalConstants.PREFIX_GROUP_URL + extractOrgFriendlyURL(userCurrentUrl) + GCubePortalConstants.USER_PROFILE_FRIENDLY_URL; // Base64.getEncoder().encodeToString(GCubeSocialNetworking.USER_PROFILE_OID.getBytes())+"="+
// Base64.getEncoder().encodeToString(username.getBytes())+"\">"+userFullName+
userFullName = "<a class=\"link\" href=\"" + profilePageURL + "?"+ // "</a> ";
Base64.getEncoder().encodeToString(GCubeSocialNetworking.USER_PROFILE_OID.getBytes())+"="+ // */
Base64.getEncoder().encodeToString(username.getBytes())+"\">"+userFullName+ //
"</a> "; // userFullName = "@"+username;
*/ //
// // write
userFullName = "@"+username; // writeProductPost(
// productTitle,
// write // productUrl,
writeProductPost( // userFullName,
productTitle, // hashtags,
productUrl, // enableNotification
userFullName, // );
hashtags, //
enableNotification // }catch(Exception e){
); // logger.error("Failed to write the post because of the following error ", e);
// }finally{
}catch(Exception e){ // SecurityTokenProvider.instance.reset();
logger.error("Failed to write the post because of the following error ", e); // ScopeProvider.instance.reset();
}finally{ // }
SecurityTokenProvider.instance.reset(); // }
ScopeProvider.instance.reset(); //
} // public static String extractOrgFriendlyURL(String portalURL) {
} // String groupRegEx = "/group/";
// if (portalURL.contains(groupRegEx)) {
public static String extractOrgFriendlyURL(String portalURL) { // String[] splits = portalURL.split(groupRegEx);
String groupRegEx = "/group/"; // String friendlyURL = splits[1];
if (portalURL.contains(groupRegEx)) { // if (friendlyURL.contains("/")) {
String[] splits = portalURL.split(groupRegEx); // friendlyURL = friendlyURL.split("/")[0];
String friendlyURL = splits[1]; // } else {
if (friendlyURL.contains("/")) { // friendlyURL = friendlyURL.split("\\?")[0].split("\\#")[0];
friendlyURL = friendlyURL.split("/")[0]; // }
} else { // return "/"+friendlyURL;
friendlyURL = friendlyURL.split("\\?")[0].split("\\#")[0]; // }
} // return null;
return "/"+friendlyURL; // }
} //
return null; // /**
} // * 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
* Send notification to vre members about the created product by writing a post. // * @param hashtags a list of product's hashtags
* @param productName the title of the product // */
* @param productUrl the url of the product // private static void writeProductPost(String productName, String productUrl, String userFullname, List<String> hashtags, boolean enablePostNotification){
* @param hashtags a list of product's hashtags //
*/ // // discover service endpoint for the social networking library
private static void writeProductPost(String productName, String productUrl, String userFullname, List<String> hashtags, boolean enablePostNotification){ // String currentScope = ScopeProvider.instance.get();
// String tokenUser = SecurityTokenProvider.instance.get();
// discover service endpoint for the social networking library //
String currentScope = ScopeProvider.instance.get(); // logger.info("Current scope for writeProductPost is " + currentScope + " and token is " + tokenUser.substring(0, 10) + "***************");
String tokenUser = SecurityTokenProvider.instance.get(); // String basePath = new GCoreEndPointReaderSocial(currentScope).getBasePath();
//
logger.info("Current scope for writeProductPost is " + currentScope + " and token is " + tokenUser.substring(0, 10) + "***************"); // if(basePath == null){
String basePath = new GCoreEndPointReaderSocial(currentScope).getBasePath(); //
// logger.error("Unable to write a post because there is no social networking service available");
if(basePath == null){ //
// }else{
logger.error("Unable to write a post because there is no social networking service available"); //
// // check base path form
}else{ // basePath = basePath.endsWith("/") ? basePath : basePath + "/";
//
// check base path form // try(CloseableHttpClient client = HttpClientBuilder.create().build();){
basePath = basePath.endsWith("/") ? basePath : basePath + "/"; //
// String pathTokenApp = basePath + SOCIAL_SERVICE_APPLICATION_TOKEN + "?gcube-token=" + tokenUser;
try(CloseableHttpClient client = HttpClientBuilder.create().build();){ // String tokenApp = requireAppToken(client, pathTokenApp);
// if(tokenApp != null){
String pathTokenApp = basePath + SOCIAL_SERVICE_APPLICATION_TOKEN + "?gcube-token=" + tokenUser; // String pathWritePost = basePath + SOCIAL_SERVICE_WRITE_APPLICATION_POST + "?gcube-token=" + tokenApp;
String tokenApp = requireAppToken(client, pathTokenApp); // writePost(client, pathWritePost, productName, productUrl, userFullname, hashtags, enablePostNotification);
if(tokenApp != null){ // }
String pathWritePost = basePath + SOCIAL_SERVICE_WRITE_APPLICATION_POST + "?gcube-token=" + tokenApp; //
writePost(client, pathWritePost, productName, productUrl, userFullname, hashtags, enablePostNotification); // }catch(Exception e){
} // logger.error("Failed to create a post", e);
// }
}catch(Exception e){ // }
logger.error("Failed to create a post", e); // }
} //
} // /**
} // * Require the application token
// * @param tokenUser
/** // * @param basePath
* Require the application token // * @param client
* @param tokenUser // * @return
* @param basePath // */
* @param client // private static String requireAppToken(CloseableHttpClient client, String path){
* @return //
*/ // String token = null;
private static String requireAppToken(CloseableHttpClient client, String path){ // try{
//
String token = null; // JSONObject request = new JSONObject();
try{ // request.put("app_id", APPLICATION_ID_CATALOGUE_MANAGER);
// HttpResponse response = performRequest(client, path, request.toJSONString());
JSONObject request = new JSONObject(); //
request.put("app_id", APPLICATION_ID_CATALOGUE_MANAGER); // int statusTokenGenerate = response.getStatusLine().getStatusCode();
HttpResponse response = performRequest(client, path, request.toJSONString()); //
// if(statusTokenGenerate == HttpURLConnection.HTTP_CREATED){
int statusTokenGenerate = response.getStatusLine().getStatusCode(); //
// // extract token
if(statusTokenGenerate == HttpURLConnection.HTTP_CREATED){ // JSONObject obj = getJSONObject(response);
// if(((Boolean) obj.get("success")))
// extract token // token = (String)obj.get("result");
JSONObject obj = getJSONObject(response); // else
if(((Boolean) obj.get("success"))) // return null;
token = (String)obj.get("result"); //
else // }else if(statusTokenGenerate == HttpURLConnection.HTTP_MOVED_TEMP
return null; // || statusTokenGenerate == HttpURLConnection.HTTP_MOVED_PERM
// || statusTokenGenerate == HttpURLConnection.HTTP_SEE_OTHER){
}else if(statusTokenGenerate == HttpURLConnection.HTTP_MOVED_TEMP //
|| statusTokenGenerate == HttpURLConnection.HTTP_MOVED_PERM // // re-execute
|| statusTokenGenerate == HttpURLConnection.HTTP_SEE_OTHER){ // Header[] locations = response.getHeaders("Location");
// Header lastLocation = locations[locations.length - 1];
// re-execute // String realLocation = lastLocation.getValue();
Header[] locations = response.getHeaders("Location"); // logger.debug("New location is " + realLocation);
Header lastLocation = locations[locations.length - 1]; // token = requireAppToken(client, realLocation);
String realLocation = lastLocation.getValue(); //
logger.debug("New location is " + realLocation); // }else
token = requireAppToken(client, realLocation); // return null;
//
}else // }catch(Exception e){
return null; // logger.error("Failed to retrieve application token", e);
// }
}catch(Exception e){ //
logger.error("Failed to retrieve application token", e); // logger.info("Returning app token " + (token != null ? token.substring(0, 10) + "*************************" : null));
} // return token;
// }
logger.info("Returning app token " + (token != null ? token.substring(0, 10) + "*************************" : null)); //
return token; // /**
} // * Write post request
// * @param client
/** // * @param applicationToken
* Write post request // * @param productName
* @param client // * @param productUrl
* @param applicationToken // * @param userFullname
* @param productName // * @param hashtags
* @param productUrl // */
* @param userFullname // private static void writePost(CloseableHttpClient client, String path, String productName, String productUrl, String userFullname, List<String> hashtags,
* @param hashtags // boolean enablePostNotification) {
*/ //
private static void writePost(CloseableHttpClient client, String path, String productName, String productUrl, String userFullname, List<String> hashtags, // try{
boolean enablePostNotification) { //
// // replace
try{ // String message = NOTIFICATION_MESSAGE.replace("$PRODUCT_TITLE", productName).replace("$PRODUCT_URL", productUrl).replace("$USER_FULLNAME", userFullname);
//
// replace // if(hashtags != null && !hashtags.isEmpty())
String message = NOTIFICATION_MESSAGE.replace("$PRODUCT_TITLE", productName).replace("$PRODUCT_URL", productUrl).replace("$USER_FULLNAME", userFullname); // for (String hashtag : hashtags) {
// String modifiedHashtag = hashtag.replaceAll(" ", "_").replace("_+", "_");
if(hashtags != null && !hashtags.isEmpty()) // if(modifiedHashtag.endsWith("_"))
for (String hashtag : hashtags) { // modifiedHashtag = modifiedHashtag.substring(0, modifiedHashtag.length() - 1);
String modifiedHashtag = hashtag.replaceAll(" ", "_").replace("_+", "_"); // message += " #" + modifiedHashtag; // ckan accepts tag with empty spaces, we don't
if(modifiedHashtag.endsWith("_")) // }
modifiedHashtag = modifiedHashtag.substring(0, modifiedHashtag.length() - 1); //
message += " #" + modifiedHashtag; // ckan accepts tag with empty spaces, we don't // JSONObject request = new JSONObject();
} // request.put("text", message);
// request.put("enable_notification", enablePostNotification);
JSONObject request = new JSONObject(); // logger.info("The post that is going to be written is ->\n" + request.toJSONString());
request.put("text", message); // HttpResponse response = performRequest(client, path, request.toJSONString());
request.put("enable_notification", enablePostNotification); // int statusWritePost = response.getStatusLine().getStatusCode();
logger.info("The post that is going to be written is ->\n" + request.toJSONString()); //
HttpResponse response = performRequest(client, path, request.toJSONString()); // if(statusWritePost == HttpURLConnection.HTTP_CREATED){
int statusWritePost = response.getStatusLine().getStatusCode(); //
// // extract token
if(statusWritePost == HttpURLConnection.HTTP_CREATED){ // JSONObject obj = getJSONObject(response);
// if(((Boolean) obj.get("success")))
// extract token // logger.info("Post written");
JSONObject obj = getJSONObject(response); // else
if(((Boolean) obj.get("success"))) // logger.info("Failed to write the post " + obj.get("message"));
logger.info("Post written"); //
else // }else if(statusWritePost == HttpURLConnection.HTTP_MOVED_TEMP
logger.info("Failed to write the post " + obj.get("message")); // || statusWritePost == HttpURLConnection.HTTP_MOVED_PERM
// || statusWritePost == HttpURLConnection.HTTP_SEE_OTHER){
}else if(statusWritePost == HttpURLConnection.HTTP_MOVED_TEMP //
|| statusWritePost == HttpURLConnection.HTTP_MOVED_PERM // // re-execute
|| statusWritePost == HttpURLConnection.HTTP_SEE_OTHER){ // Header[] locations = response.getHeaders("Location");
// Header lastLocation = locations[locations.length - 1];
// re-execute // String realLocation = lastLocation.getValue();
Header[] locations = response.getHeaders("Location"); // logger.debug("New location is " + realLocation);
Header lastLocation = locations[locations.length - 1]; // writePost(client, realLocation, productName, productUrl, userFullname, hashtags, enablePostNotification);
String realLocation = lastLocation.getValue(); //
logger.debug("New location is " + realLocation); // }else
writePost(client, realLocation, productName, productUrl, userFullname, hashtags, enablePostNotification); // throw new RuntimeException("Failed to write the post ");
//
}else // }catch(Exception e){
throw new RuntimeException("Failed to write the post "); // logger.error("Failed to write the post ", e);
// }
}catch(Exception e){ //
logger.error("Failed to write the post ", e); // }
} //
// /**
} // * Convert the json response to a map
// * @param response
/** // * @return
* Convert the json response to a map // */
* @param response // private static JSONObject getJSONObject(HttpResponse response){
* @return //
*/ // JSONObject toReturn = null;
private static JSONObject getJSONObject(HttpResponse response){ // HttpEntity entity = response.getEntity();
//
JSONObject toReturn = null; // if (entity != null) {
HttpEntity entity = response.getEntity(); // try {
// String jsonString = EntityUtils.toString(response.getEntity());
if (entity != null) { // JSONParser parser = new JSONParser();
try { // toReturn = (JSONObject)parser.parse(jsonString);
String jsonString = EntityUtils.toString(response.getEntity()); // }catch(Exception e){
JSONParser parser = new JSONParser(); // logger.error("Failed to read json object", e);
toReturn = (JSONObject)parser.parse(jsonString); // }
}catch(Exception e){ // }
logger.error("Failed to read json object", e); //
} // logger.debug("Returning " + toReturn.toJSONString());
} // return toReturn;
// }
logger.debug("Returning " + toReturn.toJSONString()); //
return toReturn; // /**
} // * Perform an http request post request with json entity
// * @throws IOException
/** // * @throws ClientProtocolException
* Perform an http request post request with json entity // */
* @throws IOException // private static HttpResponse performRequest(CloseableHttpClient client, String path, String entity) throws ClientProtocolException, IOException{
* @throws ClientProtocolException //
*/ // HttpPost request = new HttpPost(path);
private static HttpResponse performRequest(CloseableHttpClient client, String path, String entity) throws ClientProtocolException, IOException{ // StringEntity stringEntity = new StringEntity(entity);
// stringEntity.setContentType(MEDIATYPE_JSON);
HttpPost request = new HttpPost(path); // request.setEntity(stringEntity);
StringEntity stringEntity = new StringEntity(entity); // return client.execute(request);
stringEntity.setContentType(MEDIATYPE_JSON); //
request.setEntity(stringEntity); // }
return client.execute(request); //
//}
}
}

View File

@ -6,6 +6,7 @@ import java.util.Set;
import org.gcube.datacatalogue.ckanutillibrary.server.DataCatalogue; import org.gcube.datacatalogue.ckanutillibrary.server.DataCatalogue;
import org.gcube.datacatalogue.ckanutillibrary.shared.RolesCkanGroupOrOrg; import org.gcube.datacatalogue.ckanutillibrary.shared.RolesCkanGroupOrOrg;
import org.gcube.datacatalogue.ckanutillibrary.shared.jackan.model.CkanOrganization;
import org.gcube.portlets.widgets.ckandatapublisherwidget.server.CKANPublisherServicesImpl; import org.gcube.portlets.widgets.ckandatapublisherwidget.server.CKANPublisherServicesImpl;
import org.gcube.portlets.widgets.ckandatapublisherwidget.shared.OrganizationBean; import org.gcube.portlets.widgets.ckandatapublisherwidget.shared.OrganizationBean;
import org.gcube.vomanagement.usermanagement.GroupManager; import org.gcube.vomanagement.usermanagement.GroupManager;
@ -23,8 +24,6 @@ import org.gcube.vomanagement.usermanagement.model.GatewayRolesNames;
import com.liferay.portal.kernel.log.Log; import com.liferay.portal.kernel.log.Log;
import com.liferay.portal.kernel.log.LogFactoryUtil; import com.liferay.portal.kernel.log.LogFactoryUtil;
import eu.trentorise.opendata.jackan.model.CkanOrganization;
/** /**
* Facilities to check roles into the catalogue. * Facilities to check roles into the catalogue.
*/ */