a message is now sent to group administrators when a product is added to their groups

git-svn-id: http://svn.d4science-ii.research-infrastructures.eu/gcube/trunk/portlets/widgets/ckan-metadata-publisher-widget@134315 82a268e6-3cf1-43bd-a215-b396298e98cf
This commit is contained in:
Costantino Perciante 2016-11-17 13:08:22 +00:00
parent cb02953847
commit cf300cf7e3
12 changed files with 451 additions and 287 deletions

View File

@ -121,6 +121,11 @@
<artifactId>aslcore</artifactId> <artifactId>aslcore</artifactId>
<scope>provided</scope> <scope>provided</scope>
</dependency> </dependency>
<dependency>
<groupId>org.gcube.portal</groupId>
<artifactId>social-networking-library</artifactId>
<scope>provided</scope>
</dependency>
<dependency> <dependency>
<groupId>org.gcube.portal</groupId> <groupId>org.gcube.portal</groupId>
<artifactId>custom-portal-handler</artifactId> <artifactId>custom-portal-handler</artifactId>

View File

@ -5,19 +5,18 @@ import java.util.List;
import org.gcube.portlets.widgets.ckandatapublisherwidget.client.ui.CreateDatasetForm; import org.gcube.portlets.widgets.ckandatapublisherwidget.client.ui.CreateDatasetForm;
import org.gcube.portlets.widgets.ckandatapublisherwidget.client.ui.MetaDataFieldSkeleton; import org.gcube.portlets.widgets.ckandatapublisherwidget.client.ui.MetaDataFieldSkeleton;
import org.gcube.portlets.widgets.ckandatapublisherwidget.client.ui.TwinColumnSelection.TwinColumnSelectionMainPanel;
import org.gcube.portlets.widgets.ckandatapublisherwidget.shared.DataType; import org.gcube.portlets.widgets.ckandatapublisherwidget.shared.DataType;
import org.gcube.portlets.widgets.ckandatapublisherwidget.shared.MetadataFieldWrapper; import org.gcube.portlets.widgets.ckandatapublisherwidget.shared.MetadataFieldWrapper;
import org.gcube.portlets.widgets.ckandatapublisherwidget.shared.ResourceElementBean;
import com.github.gwtbootstrap.client.ui.Button; import com.github.gwtbootstrap.client.ui.Button;
import com.github.gwtbootstrap.client.ui.ListBox;
import com.google.gwt.core.client.EntryPoint; import com.google.gwt.core.client.EntryPoint;
import com.google.gwt.core.shared.GWT; import com.google.gwt.core.shared.GWT;
import com.google.gwt.dom.client.SelectElement;
import com.google.gwt.event.dom.client.ClickEvent; import com.google.gwt.event.dom.client.ClickEvent;
import com.google.gwt.event.dom.client.ClickHandler; import com.google.gwt.event.dom.client.ClickHandler;
import com.google.gwt.event.shared.HandlerManager; import com.google.gwt.event.shared.HandlerManager;
import com.google.gwt.user.client.Window; import com.google.gwt.user.client.Window;
import com.google.gwt.user.client.rpc.AsyncCallback;
import com.google.gwt.user.client.ui.RootPanel; import com.google.gwt.user.client.ui.RootPanel;
import com.google.gwt.user.client.ui.VerticalPanel; import com.google.gwt.user.client.ui.VerticalPanel;
@ -40,7 +39,35 @@ public class CKanMetadataPublisher implements EntryPoint {
//startExample(); //startExample();
//testMetadata(); //testMetadata();
//testSelectionPanel(); //testSelectionPanel();
//testHideOption();
}
@SuppressWarnings("unused")
private void testHideOption() {
ListBox listBox = new ListBox(true);
listBox.addItem("A");
listBox.addItem("B");
listBox.addItem("C");
listBox.addItem("D");
listBox.addItem("E");
listBox.addItem("F");
List<String> toHide = new ArrayList<String>();
toHide.add("A");
toHide.add("D");
RootPanel.get("ckan-metadata-publisher-div").add(listBox);
SelectElement se = listBox.getElement().cast();
// hide
for (int i = 0; i < listBox.getItemCount(); i++) {
if(toHide.contains(listBox.getItemText(i))){
GWT.log("to hide " + listBox.getItemText(i));
se.getOptions().getItem(i).getStyle().setProperty("display", "none");
}
}
} }
@SuppressWarnings("unused") @SuppressWarnings("unused")
@ -73,24 +100,24 @@ public class CKanMetadataPublisher implements EntryPoint {
// //
String folderId = "e87bfc7d-4fb0-4795-9c79-0c495500ca9c"; // String folderId = "e87bfc7d-4fb0-4795-9c79-0c495500ca9c";
ckanServices.getTreeFolder(folderId, new AsyncCallback<ResourceElementBean>() { // ckanServices.getTreeFolder(folderId, new AsyncCallback<ResourceElementBean>() {
//
//
@Override // @Override
public void onSuccess(ResourceElementBean result) { // public void onSuccess(ResourceElementBean result) {
if(result != null){ // if(result != null){
RootPanel.get("ckan-metadata-publisher-div").add(new TwinColumnSelectionMainPanel(result)); // RootPanel.get("ckan-metadata-publisher-div").add(new TwinColumnSelectionMainPanel(result));
} // }
} // }
//
@Override // @Override
public void onFailure(Throwable caught) { // public void onFailure(Throwable caught) {
//
Window.alert("Failed to retrieve ResourceElementBean"); // Window.alert("Failed to retrieve ResourceElementBean");
//
} // }
}); // });
} }
@SuppressWarnings("unused") @SuppressWarnings("unused")

View File

@ -67,17 +67,17 @@ public interface CKanPublisherService extends RemoteService {
* @return true if it exists, false otherwise * @return true if it exists, false otherwise
*/ */
boolean datasetIdAlreadyExists(String title); boolean datasetIdAlreadyExists(String title);
/** /**
* Retrieve the list of groups the user can choose to associate this product with. * Retrieve the list of groups the user can choose to associate this product with.
* @return a list of groups' beans * @return a list of groups' beans
*/ */
List<GroupBean> getUserGroups(); List<GroupBean> getUserGroups();
/** // /**
* Return a tree object representing the whole folder hierarchy // * Return a tree object representing the whole folder hierarchy
* @param folderId // * @param folderId
* @return ResourceElementBean // * @return ResourceElementBean
*/ // */
ResourceElementBean getTreeFolder(String folderId); // ResourceElementBean getTreeFolder(String folderId);
} }

View File

@ -68,18 +68,17 @@ public interface CKanPublisherServiceAsync {
*/ */
void datasetIdAlreadyExists(String title, AsyncCallback<Boolean> callback); void datasetIdAlreadyExists(String title, AsyncCallback<Boolean> callback);
/** // /**
* Return a tree object representing the whole folder hierarchy // * Return a tree object representing the whole folder hierarchy
* @param folderId // * @param folderId
* @return ResourceElementBean // * @return ResourceElementBean
*/ // */
void getTreeFolder(String folderId, // void getTreeFolder(String folderId,
AsyncCallback<ResourceElementBean> callback); // AsyncCallback<ResourceElementBean> callback);
/** /**
* Retrieve the list of groups the user can choose to associate this product with. * Retrieve the list of groups the user can choose to associate this product with.
* @return a list of groups' beans * @return a list of groups' beans
*/ */
void getUserGroups(AsyncCallback<List<GroupBean>> callback); void getUserGroups(AsyncCallback<List<GroupBean>> callback);
} }

View File

@ -43,6 +43,7 @@ import com.github.gwtbootstrap.client.ui.constants.AlertType;
import com.github.gwtbootstrap.client.ui.constants.ControlGroupType; import com.github.gwtbootstrap.client.ui.constants.ControlGroupType;
import com.github.gwtbootstrap.client.ui.resources.Bootstrap.Tabs; import com.github.gwtbootstrap.client.ui.resources.Bootstrap.Tabs;
import com.google.gwt.core.client.GWT; import com.google.gwt.core.client.GWT;
import com.google.gwt.dom.client.SelectElement;
import com.google.gwt.dom.client.Style.Unit; import com.google.gwt.dom.client.Style.Unit;
import com.google.gwt.event.dom.client.ChangeEvent; import com.google.gwt.event.dom.client.ChangeEvent;
import com.google.gwt.event.dom.client.ChangeHandler; import com.google.gwt.event.dom.client.ChangeHandler;
@ -165,8 +166,8 @@ public class CreateDatasetForm extends Composite{
// error/info messages // error/info messages
protected static final String ERROR_PRODUCT_CREATION = "There was an error while trying to publish your product, sorry.. Retry later"; protected static final String ERROR_PRODUCT_CREATION = "There was an error while trying to publish your product, sorry.. Retry later";
protected static final String PRODUCT_CREATED_OK = "Product correctly created!"; protected static final String PRODUCT_CREATED_OK = "Product correctly published!";
private static final String TRYING_TO_CREATE_PRODUCT = "Trying to create product, please wait"; private static final String TRYING_TO_CREATE_PRODUCT = "Trying to publish the product, please wait";
// tab panel // tab panel
private TabPanel tabPanel; private TabPanel tabPanel;
@ -209,9 +210,7 @@ public class CreateDatasetForm extends Composite{
* @param eventBus the event bus * @param eventBus the event bus
*/ */
public CreateDatasetForm(HandlerManager eventBus) { public CreateDatasetForm(HandlerManager eventBus) {
createDatasetFormBody(false, null, eventBus); createDatasetFormBody(false, null, eventBus);
} }
/** /**
@ -220,40 +219,30 @@ public class CreateDatasetForm extends Composite{
* @param eventBus the event bus * @param eventBus the event bus
*/ */
public CreateDatasetForm(String idFolderWorkspace, HandlerManager eventBus) { public CreateDatasetForm(String idFolderWorkspace, HandlerManager eventBus) {
createDatasetFormBody(true, idFolderWorkspace, eventBus); createDatasetFormBody(true, idFolderWorkspace, eventBus);
} }
/** /**
* Bind on events * Bind on events
*/ */
private void bind() { private void bind() {
// when a custom field is removed, remove it from the list // when a custom field is removed, remove it from the list
eventBus.addHandler(DeleteCustomFieldEvent.TYPE, new DeleteCustomFieldEventHandler() { eventBus.addHandler(DeleteCustomFieldEvent.TYPE, new DeleteCustomFieldEventHandler() {
@Override @Override
public void onRemoveEntry(DeleteCustomFieldEvent event) { public void onRemoveEntry(DeleteCustomFieldEvent event) {
customFieldEntriesList.remove(event.getRemovedEntry()); customFieldEntriesList.remove(event.getRemovedEntry());
customFields.remove(event.getRemovedEntry()); customFields.remove(event.getRemovedEntry());
} }
}); });
// on close form // on close form
eventBus.addHandler(CloseCreationFormEvent.TYPE, new CloseCreationFormEventHandler() { eventBus.addHandler(CloseCreationFormEvent.TYPE, new CloseCreationFormEventHandler() {
@Override @Override
public void onClose(CloseCreationFormEvent event) { public void onClose(CloseCreationFormEvent event) {
InfoIconsLabels.closeDialogBox(popupOpenedIds); InfoIconsLabels.closeDialogBox(popupOpenedIds);
} }
}); });
} }
/** /**
@ -284,7 +273,7 @@ public class CreateDatasetForm extends Composite{
@Override @Override
public void onFailure(Throwable caught) { public void onFailure(Throwable caught) {
setAlertBlock("Error while retrieving information, try to refresh the page", AlertType.ERROR, true); setAlertBlock("Error while retrieving information, try to refresh the page and retry", AlertType.ERROR, true);
} }
@ -293,7 +282,7 @@ public class CreateDatasetForm extends Composite{
if(bean == null){ if(bean == null){
setAlertBlock("Error while retrieving information, try to refresh the page", AlertType.ERROR, true); setAlertBlock("Error while retrieving information, try to refresh the page and retry", AlertType.ERROR, true);
} }
else{ else{
@ -381,14 +370,14 @@ public class CreateDatasetForm extends Composite{
} }
@Override @Override
public void onSuccess(List<MetaDataProfileBean> result) { public void onSuccess(final List<MetaDataProfileBean> profiles) {
if(result == null){ if(profiles == null){
setAlertBlock("Error while retrieving profiles, try later", AlertType.ERROR, true); setAlertBlock("Error while retrieving profiles, try later", AlertType.ERROR, true);
} }
else{ else{
receivedBean.setMetadataList(result); receivedBean.setMetadataList(profiles);
prepareMetadataList(receivedBean); prepareMetadataList(receivedBean);
organizationsListbox.setEnabled(true); organizationsListbox.setEnabled(true);
metadataProfilesFormatListbox.setEnabled(true); metadataProfilesFormatListbox.setEnabled(true);
@ -429,17 +418,21 @@ public class CreateDatasetForm extends Composite{
ckanServices.getUserGroups(new AsyncCallback<List<GroupBean>>() { ckanServices.getUserGroups(new AsyncCallback<List<GroupBean>>() {
@Override @Override
public void onSuccess(List<GroupBean> result) { public void onSuccess(List<GroupBean> groups) {
if(result == null){ if(groups == null){
setAlertBlock("Error while retrieving groups, try later", AlertType.ERROR, true); setAlertBlock("Error while retrieving groups, try later", AlertType.ERROR, true);
}else{ }else{
if(result.isEmpty()) if(groups.isEmpty())
return; return;
else{ else{
for (GroupBean groups : result) {
groupsListbox.addItem(groups.getGroupTitle(), groups.getGroupName()); // add groups
for (GroupBean group : groups) {
groupsListbox.addItem(group.getGroupTitle(), group.getGroupName());
} }
hideGroupsAlreadyInProfile(profiles);
groupsControlGroup.setVisible(true); groupsControlGroup.setVisible(true);
} }
// everything went ok // everything went ok
@ -469,16 +462,14 @@ public class CreateDatasetForm extends Composite{
} }
/** /**
* When the organization name is changed we need to retrieve the list of profiles * When the organization name is changed we need to retrieve the list of profiles and groups
*/ */
private void organizationsListboxChangeHandlerBody() { private void organizationsListboxChangeHandlerBody() {
// remove any other product profiles // remove any other product profiles
int presentItems = metadataProfilesFormatListbox.getItemCount(); metadataProfilesFormatListbox.clear();
for (int i = presentItems - 1; i >= 0; i--) {
metadataProfilesFormatListbox.removeItem(i);
}
// add "none" item again // add "none" item again
metadataProfilesFormatListbox.addItem(NONE_PROFILE); metadataProfilesFormatListbox.addItem(NONE_PROFILE);
@ -494,24 +485,61 @@ public class CreateDatasetForm extends Composite{
setAlertBlock("Retrieving profiles, please wait...", AlertType.INFO, true); setAlertBlock("Retrieving profiles, please wait...", AlertType.INFO, true);
// disable the list of organizations name so that the user doesn't change it again // disable the list of organizations name so that the user doesn't change it again
// also disable the profiles and the list of groups
organizationsListbox.setEnabled(false); organizationsListbox.setEnabled(false);
metadataProfilesFormatListbox.setEnabled(false); metadataProfilesFormatListbox.setEnabled(false);
groupsListbox.setEnabled(false);
groupsControlGroup.setVisible(false);
// perform remote request of profiles for the selected organization // perform remote request of profiles for the selected organization
ckanServices.getProfiles(orgName, new AsyncCallback<List<MetaDataProfileBean>>() { ckanServices.getProfiles(orgName, new AsyncCallback<List<MetaDataProfileBean>>() {
@Override @Override
public void onSuccess(List<MetaDataProfileBean> result) { public void onSuccess(final List<MetaDataProfileBean> profiles) {
if(result != null){ if(profiles != null){
receivedBean.setMetadataList(result); receivedBean.setMetadataList(profiles);
prepareMetadataList(receivedBean); prepareMetadataList(receivedBean);
organizationsListbox.setEnabled(true); organizationsListbox.setEnabled(true);
metadataProfilesFormatListbox.setEnabled(true); metadataProfilesFormatListbox.setEnabled(true);
groupsListbox.setEnabled(true);
// everything went ok // try to retrieve the licenses
setAlertBlock("", AlertType.DEFAULT, false); setAlertBlock("Retrieving groups, please wait...", AlertType.INFO, true);
// request groups
ckanServices.getUserGroups(new AsyncCallback<List<GroupBean>>() {
@Override
public void onSuccess(List<GroupBean> groups) {
if(groups == null){
setAlertBlock("Error while retrieving groups, try later", AlertType.ERROR, true);
}else{
if(groups.isEmpty())
return;
else{
// add groups
for (GroupBean group : groups) {
groupsListbox.addItem(group.getGroupTitle(), group.getGroupName());
}
hideGroupsAlreadyInProfile(profiles);
groupsListbox.setEnabled(true);
groupsControlGroup.setVisible(true);
}
// everything went ok
setAlertBlock("", AlertType.ERROR, false);
}
}
@Override
public void onFailure(Throwable caught) {
setAlertBlock("Error while retrieving groups, try later", AlertType.ERROR, true);
}
});
}else }else
setAlertBlock("Error while retrieving profiles, sorry", AlertType.ERROR, true); setAlertBlock("Error while retrieving profiles, sorry", AlertType.ERROR, true);
@ -534,10 +562,10 @@ public class CreateDatasetForm extends Composite{
*/ */
private void prepareMetadataList(final DatasetMetadataBean receivedBean) { private void prepareMetadataList(final DatasetMetadataBean receivedBean) {
List<MetaDataProfileBean> beans = receivedBean.getMetadataList(); List<MetaDataProfileBean> profiles = receivedBean.getMetadataList();
if(beans != null && !beans.isEmpty()){ if(profiles != null && !profiles.isEmpty()){
for(MetaDataProfileBean metadataBean: beans){ for(MetaDataProfileBean metadataBean: profiles){
metadataProfilesFormatListbox.addItem(metadataBean.getType().getName()); metadataProfilesFormatListbox.addItem(metadataBean.getType().getName());
@ -547,21 +575,37 @@ public class CreateDatasetForm extends Composite{
@Override @Override
public void onChange(ChangeEvent event) { public void onChange(ChangeEvent event) {
String selectedItem = metadataProfilesFormatListbox.getSelectedItemText(); String selectedItemText = metadataProfilesFormatListbox.getSelectedItemText();
if(selectedItem.equals(NONE_PROFILE)){ if(selectedItemText.equals(NONE_PROFILE)){
// hide the panel
metadataFieldsPanel.clear(); metadataFieldsPanel.clear();
metadataFieldsPanel.setVisible(false); metadataFieldsPanel.setVisible(false);
receivedBean.setChosenProfile(null); receivedBean.setChosenProfile(null);
}else{ }else{
receivedBean.setChosenProfile(selectedItem); receivedBean.setChosenProfile(selectedItemText);
metadataFieldsPanel.clear(); metadataFieldsPanel.clear();
addFields(selectedItem); addFields(selectedItemText);
} }
} }
}); });
} }
// hide elements or show them if needed (groups in profiles cannot be present again in groups listbox)
if(groupsControlGroup.isVisible()){
List<String> groupsToHide = new ArrayList<String>();
for(MetaDataProfileBean profile: profiles)
groupsToHide.add(profile.getType().toString());
SelectElement se = groupsListbox.getElement().cast();
for (int i = 0; i < groupsListbox.getItemCount(); i++) {
if(groupsToHide.contains(groupsListbox.getItemText(i))){
se.getOptions().getItem(i).getStyle().setProperty("display", "none");
}else
se.getOptions().getItem(i).getStyle().setProperty("display", "");
}
}
metadataProfilesControlGroup.setVisible(true); metadataProfilesControlGroup.setVisible(true);
}else{ }else{
// just hide this listbox // just hide this listbox
@ -635,23 +679,16 @@ public class CreateDatasetForm extends Composite{
@Override @Override
public void onSuccess(Boolean result) { public void onSuccess(Boolean result) {
if(result){ if(result){
alertOnContinue("Sorry but a product with such title already exists, try to change it", AlertType.WARNING); alertOnContinue("Sorry but a product with such title already exists, try to change it", AlertType.WARNING);
}else{ }else{
actionsAfterOnContinue(); actionsAfterOnContinue();
} }
} }
@Override @Override
public void onFailure(Throwable caught) { public void onFailure(Throwable caught) {
alertOnContinue("Sorry but there was a problem while checking if the inserted data are correct", AlertType.ERROR); alertOnContinue("Sorry but there was a problem while checking if the inserted data are correct", AlertType.ERROR);
} }
}); });
} }
@ -1228,6 +1265,7 @@ public class CreateDatasetForm extends Composite{
organizationsListbox.setEnabled(false); organizationsListbox.setEnabled(false);
addCustomFieldButton.setEnabled(false); addCustomFieldButton.setEnabled(false);
metadataProfilesFormatListbox.setEnabled(false); metadataProfilesFormatListbox.setEnabled(false);
groupsListbox.setEnabled(false);
for(CustomFieldEntry ce: customFieldEntriesList) for(CustomFieldEntry ce: customFieldEntriesList)
ce.freeze(); ce.freeze();
@ -1290,4 +1328,23 @@ public class CreateDatasetForm extends Composite{
} }
/**
* Hide the groups that are already listed in the profiles page
* @param profiles
*/
private void hideGroupsAlreadyInProfile(List<MetaDataProfileBean> profiles) {
List<String> groupsToHide = new ArrayList<String>();
for(MetaDataProfileBean profile: profiles)
groupsToHide.add(profile.getType().getName());
SelectElement se = groupsListbox.getElement().cast();
for (int i = 0; i < groupsListbox.getItemCount(); i++) {
if(groupsToHide.contains(groupsListbox.getItemText(i))){
se.getOptions().getItem(i).getStyle().setProperty("display", "none");
}
}
}
} }

View File

@ -1,87 +0,0 @@
package org.gcube.portlets.widgets.ckandatapublisherwidget.server;
import java.util.List;
import org.gcube.datacatalogue.ckanutillibrary.DataCatalogue;
import org.gcube.datacatalogue.ckanutillibrary.models.RolesCkanGroupOrOrg;
import org.gcube.portlets.widgets.ckandatapublisherwidget.shared.GroupBean;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import eu.trentorise.opendata.jackan.model.CkanGroup;
/**
* Associate the dataset to a group.
* @author Costantino Perciante at ISTI-CNR (costantino.perciante@isti.cnr.it)
*/
public class AssociationToGroupThread extends Thread {
private static final Logger logger = LoggerFactory.getLogger(AssociationToGroupThread.class);
private String groupTitle;
private String datasetId;
private String username;
private DataCatalogue catalogue;
private String organization;
private List<GroupBean> groups;
/**
* @param list
* @param groupTitle
* @param datasetId
* @param username
* @param catalogue
*/
public AssociationToGroupThread(List<GroupBean> groups, String groupTitle, String datasetId,
String username, DataCatalogue catalogue, String organization) {
this.groups = groups;
this.groupTitle = groupTitle;
this.datasetId = datasetId;
this.username = username;
this.catalogue = catalogue;
this.organization = organization;
}
@Override
public void run() {
logger.info("Association thread started to put the dataset with id = "+ datasetId + " into group with title " + groupTitle + " for user " + username);
// create the group
CkanGroup group = catalogue.createGroup(groupTitle, groupTitle, "");
if(group == null){
logger.warn("The group doesn't exist!!! Unable to perform such association");
}else{
logger.debug("Group exists, going to add the user " + username + " as its admin...");
// retrieve the role to be assigned according the one the user has into the organization of the dataset
RolesCkanGroupOrOrg role = RolesCkanGroupOrOrg.valueOf(catalogue.getRoleOfUserInOrganization(username, organization, catalogue.getApiKeyFromUsername(username)).toUpperCase());
if(!role.equals(RolesCkanGroupOrOrg.ADMIN))
role = RolesCkanGroupOrOrg.MEMBER; // decrease the role to member if it is not an admin
boolean assigned = catalogue.checkRoleIntoGroup(username, groupTitle, role);
if(assigned){
logger.debug("Admin/editor role was assigned for this group, going to associate the product to the group");
boolean putIntoGroup = catalogue.assignDatasetToGroup(groupTitle, datasetId, catalogue.getApiKeyFromUsername(username));
logger.info("Was product put into group? " + putIntoGroup);
}
}
logger.info("Other groups to which the product should be associate are " + groups);
for (GroupBean groupBean : groups) {
boolean putIntoGroup = catalogue.assignDatasetToGroup(groupBean.getGroupTitle(), datasetId, catalogue.getApiKeyFromUsername(username));
logger.info("Was product put into group" + groupBean + "? " + putIntoGroup);
}
}
}

View File

@ -12,8 +12,6 @@ import javax.servlet.http.HttpSession;
import org.gcube.application.framework.core.session.ASLSession; import org.gcube.application.framework.core.session.ASLSession;
import org.gcube.application.framework.core.session.SessionManager; import org.gcube.application.framework.core.session.SessionManager;
import org.gcube.common.homelibrary.home.HomeLibrary;
import org.gcube.common.homelibrary.home.workspace.Workspace;
import org.gcube.datacatalogue.ckanutillibrary.DataCatalogue; import org.gcube.datacatalogue.ckanutillibrary.DataCatalogue;
import org.gcube.datacatalogue.ckanutillibrary.DataCatalogueFactory; import org.gcube.datacatalogue.ckanutillibrary.DataCatalogueFactory;
import org.gcube.datacatalogue.ckanutillibrary.models.ResourceBean; import org.gcube.datacatalogue.ckanutillibrary.models.ResourceBean;
@ -21,6 +19,8 @@ import org.gcube.datacatalogue.ckanutillibrary.utils.SessionCatalogueAttributes;
import org.gcube.datacatalogue.ckanutillibrary.utils.UtilMethods; import org.gcube.datacatalogue.ckanutillibrary.utils.UtilMethods;
import org.gcube.portal.custom.scopemanager.scopehelper.ScopeHelper; import org.gcube.portal.custom.scopemanager.scopehelper.ScopeHelper;
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.Utils; import org.gcube.portlets.widgets.ckandatapublisherwidget.server.utils.Utils;
import org.gcube.portlets.widgets.ckandatapublisherwidget.server.utils.WorkspaceUtils; import org.gcube.portlets.widgets.ckandatapublisherwidget.server.utils.WorkspaceUtils;
import org.gcube.portlets.widgets.ckandatapublisherwidget.shared.DatasetMetadataBean; import org.gcube.portlets.widgets.ckandatapublisherwidget.shared.DatasetMetadataBean;
@ -52,7 +52,6 @@ public class CKANPublisherServicesImpl extends RemoteServiceServlet implements C
public static final String TEST_SCOPE = "/gcube"; public static final String TEST_SCOPE = "/gcube";
public static final String TEST_USER = "test.user"; public static final String TEST_USER = "test.user";
private final static String TEST_SEC_TOKEN = "a1e19695-467f-42b8-966d-bf83dd2382ef";
// map <orgName, scope> // map <orgName, scope>
private ConcurrentHashMap<String, String> mapOrganizationScope = new ConcurrentHashMap<String, String>(); private ConcurrentHashMap<String, String> mapOrganizationScope = new ConcurrentHashMap<String, String>();
@ -71,7 +70,7 @@ public class CKANPublisherServicesImpl extends RemoteServiceServlet implements C
logger.debug("Discovering ckan instance into scope " + scopeInWhichDiscover); logger.debug("Discovering ckan instance into scope " + scopeInWhichDiscover);
instance = DataCatalogueFactory.getFactory().getUtilsPerScope(scopeInWhichDiscover); instance = DataCatalogueFactory.getFactory().getUtilsPerScope(scopeInWhichDiscover);
}catch(Exception e){ }catch(Exception e){
logger.warn("Unable to retrieve ckan utils in scope " + scopeInWhichDiscover + ". Error is " + e.toString()); logger.warn("Unable to retrieve ckan utils in scope " + scopeInWhichDiscover + ". Error is " + e.getLocalizedMessage());
} }
return instance; return instance;
} }
@ -102,54 +101,6 @@ public class CKANPublisherServicesImpl extends RemoteServiceServlet implements C
return user; return user;
} }
/**
* Get current user's token (for a given scope)
* @param scope if it is not specified it will be retrieved from the asl
* @return String the ckan user's token
*/
private String getUserCKanTokenFromSession(String scope){
String token = null;
if(!isWithinPortal()){
logger.warn("You are running outside the portal");
token = TEST_SEC_TOKEN;
}else{
ASLSession aslSession = getASLSession();
String username = aslSession.getUsername();
logger.debug("User in session is " + username);
if(username.equals(TEST_USER)){
logger.warn("Session expired, returning null token");
token = null;
}else{
try{
HttpSession httpSession = getThreadLocalRequest().getSession();
String scopeInWhichDiscover = (scope != null && !scope.isEmpty()) ? scope : getASLSession().getScope();
String keyPerScope = UtilMethods.concatenateSessionKeyScope(SessionCatalogueAttributes.CKAN_TOKEN_KEY, scopeInWhichDiscover);
if(httpSession.getAttribute(keyPerScope) != null){
token = (String)httpSession.getAttribute(keyPerScope);
logger.debug("Found ckan token into session");
}
else{
token = getCatalogue(scopeInWhichDiscover).getApiKeyFromUsername(username);
httpSession.setAttribute(keyPerScope, token);
logger.debug("Ckan token has been set for user " + username);
}
logger.debug("Found ckan token " + token.substring(0, 3) + "************************" +
" for user " + username + " into scope " + scopeInWhichDiscover);
}catch(Exception e){
logger.error("Error while retrieving the key" , e);
}
}
}
return token;
}
/** /**
* Retrieve the list of organizations in which the user can publish (roles ADMIN) * Retrieve the list of organizations in which the user can publish (roles ADMIN)
* @param username * @param username
@ -367,8 +318,9 @@ 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 userApiKey = utils.getApiKeyFromUsername(userName);
String datasetId = utils.createCKanDataset(getUserCKanTokenFromSession(scope), title, null, organizationNameOrId, author, String datasetId = utils.createCKanDataset(userApiKey, title, null, organizationNameOrId, author,
authorMail, maintainer, maintainerMail, version, description, licenseId, authorMail, maintainer, maintainerMail, version, description, licenseId,
listOfTags, customFields, resources, setPublic); listOfTags, customFields, resources, setPublic);
@ -378,20 +330,23 @@ public class CKANPublisherServicesImpl extends RemoteServiceServlet implements C
toCreate.setId(datasetId); toCreate.setId(datasetId);
// retrieve the url // retrieve the url
String datasetUrl = utils.getPortletUrl() + "?" + URLEncoder.encode("path=" + utils.getUrlFromDatasetIdOrName(getUserCKanTokenFromSession(scope), datasetId, true), "UTF-8"); String datasetUrl = utils.getPortletUrl() + "?" + URLEncoder.encode("path=" + utils.getUrlFromDatasetIdOrName(userApiKey, datasetId, true), "UTF-8");
toCreate.setSource(datasetUrl); toCreate.setSource(datasetUrl);
// start a thread that will associate this dataset with the group // start a thread that will associate this dataset with the group
if(toCreate.getChosenProfile() != null || toCreate.getGroups() != null){ if(toCreate.getChosenProfile() != null || toCreate.getGroups() != null){
AssociationToGroupThread threadAssociationToGroup = AssociationToGroupAndNotifyThread threadAssociationToGroup =
new AssociationToGroupThread( new AssociationToGroupAndNotifyThread(
toCreate.getGroups(), toCreate.getGroups(),
toCreate.getChosenProfile(), toCreate.getChosenProfile(),
datasetId, datasetId,
toCreate.getTitle(),
aslSession.getUserFullName(),
userName, userName,
utils, utils,
organizationNameOrId organizationNameOrId,
getThreadLocalRequest()
); );
threadAssociationToGroup.start(); threadAssociationToGroup.start();
@ -406,7 +361,7 @@ public class CKANPublisherServicesImpl extends RemoteServiceServlet implements C
datasetUrl, datasetUrl,
false, // send notification to other people false, // send notification to other people
toCreate.getTags(), toCreate.getTags(),
toCreate.getAuthorFullName() aslSession.getUserFullName()
); );
threadWritePost.start(); threadWritePost.start();
@ -452,7 +407,8 @@ 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());
String resourceId = getCatalogue(scope).addResourceToDataset(resourceBean, getUserCKanTokenFromSession(scope)); DataCatalogue catalogue = getCatalogue(scope);
String resourceId = catalogue.addResourceToDataset(resourceBean, catalogue.getApiKeyFromUsername(username));
if(resourceId != null){ if(resourceId != null){
logger.debug("Resource " + resource.getName() + " is now available"); logger.debug("Resource " + resource.getName() + " is now available");
@ -490,8 +446,9 @@ public class CKANPublisherServicesImpl extends RemoteServiceServlet implements C
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());
deleted = getCatalogue(scope). DataCatalogue catalogue = getCatalogue(scope);
deleteResourceFromDataset(resource.getOriginalIdInWorkspace(), getUserCKanTokenFromSession(scope)); deleted = catalogue.
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
@ -562,46 +519,46 @@ public class CKANPublisherServicesImpl extends RemoteServiceServlet implements C
return toReturn; return toReturn;
} }
@Override // @Override
public ResourceElementBean getTreeFolder(String folderId) { // public ResourceElementBean getTreeFolder(String folderId) {
//
if(folderId == null || folderId.isEmpty()){ // if(folderId == null || folderId.isEmpty()){
logger.warn("Empty folder id or null, returning"); // logger.warn("Empty folder id or null, returning");
return null; // return null;
} // }
ASLSession session = getASLSession(); // ASLSession session = getASLSession();
try{ // try{
if(!isWithinPortal()){ // if(!isWithinPortal()){
logger.warn("Running outside the portal"); // logger.warn("Running outside the portal");
Workspace ws = getFakeWS(); // Workspace ws = getFakeWS();
ResourceElementBean toReturn = WorkspaceUtils.getTreeFromFolder(folderId, ws); // ResourceElementBean toReturn = WorkspaceUtils.getTreeFromFolder(folderId, ws);
logger.debug("Returning " + toReturn); // logger.debug("Returning " + toReturn);
return toReturn; // return toReturn;
}else{ // }else{
if(session.getUsername().equals(TEST_USER)){ // if(session.getUsername().equals(TEST_USER)){
return null; // return null;
}else{ // }else{
// TODO // // TODO
return null; // return null;
} // }
} // }
}catch(Exception e){ // }catch(Exception e){
logger.error("Failed to build the tree", e); // logger.error("Failed to build the tree", e);
} // }
return null; // return null;
} // }
//
/** // /**
* Retrieve the workspace for the development user // * Retrieve the workspace for the development user
* @return // * @return
* @throws Exception // * @throws Exception
*/ // */
private Workspace getFakeWS() throws Exception{ // private Workspace getFakeWS() throws Exception{
return HomeLibrary // return HomeLibrary
.getHomeManagerFactory() // .getHomeManagerFactory()
.getHomeManager() // .getHomeManager()
.getHome(getDevelopmentUser()).getWorkspace(); // .getHome(getDevelopmentUser()).getWorkspace();
} // }
@Override @Override
public List<GroupBean> getUserGroups() { public List<GroupBean> getUserGroups() {
@ -611,6 +568,7 @@ public class CKANPublisherServicesImpl extends RemoteServiceServlet implements C
if(isWithinPortal()){ if(isWithinPortal()){
String username = session.getUsername(); String username = session.getUsername();
if(username.equals(TEST_USER)){ if(username.equals(TEST_USER)){
logger.warn("Session expired"); logger.warn("Session expired");
return null; return null;
@ -623,10 +581,16 @@ public class CKANPublisherServicesImpl extends RemoteServiceServlet implements C
String scope = (String)httpSession.getAttribute(SessionCatalogueAttributes.SCOPE_CLIENT_PORTLET_URL); String scope = (String)httpSession.getAttribute(SessionCatalogueAttributes.SCOPE_CLIENT_PORTLET_URL);
DataCatalogue catalogue = getCatalogue(scope); DataCatalogue catalogue = getCatalogue(scope);
List<CkanGroup> ckanGroups = catalogue.getGroups(); List<CkanGroup> ckanGroups = catalogue.getGroups();
String apiKey = catalogue.getApiKeyFromUsername(username);
// TODO check role // Members/Admin of the group
for (CkanGroup ckanGroup : ckanGroups) { for (CkanGroup ckanGroup : ckanGroups) {
String role = catalogue.getRoleOfUserInGroup(username, ckanGroup.getName(), apiKey);
if(role == null)
continue;
toReturn.add(new GroupBean(ckanGroup.getTitle(), ckanGroup.getName())); toReturn.add(new GroupBean(ckanGroup.getTitle(), ckanGroup.getName()));
} }
logger.debug("List of groups to return is " + toReturn); logger.debug("List of groups to return is " + toReturn);

View File

@ -0,0 +1,166 @@
package org.gcube.portlets.widgets.ckandatapublisherwidget.server.threads;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import javax.servlet.http.HttpServletRequest;
import org.gcube.common.portal.mailing.EmailNotification;
import org.gcube.datacatalogue.ckanutillibrary.DataCatalogue;
import org.gcube.datacatalogue.ckanutillibrary.models.RolesCkanGroupOrOrg;
import org.gcube.datacatalogue.ckanutillibrary.utils.UtilMethods;
import org.gcube.portlets.widgets.ckandatapublisherwidget.shared.GroupBean;
import org.gcube.vomanagement.usermanagement.UserManager;
import org.gcube.vomanagement.usermanagement.impl.LiferayUserManager;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
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)
*/
public class AssociationToGroupAndNotifyThread extends Thread {
private static final Logger logger = LoggerFactory.getLogger(AssociationToGroupAndNotifyThread.class);
private static final String PRODUCT_ASSOCIATED_TO_GROUP_SUBJECT = "Product $PRODUCT added to group $GROUP";
private static final String PRODUCT_ASSOCIATED_TO_GROUP_BODY = "Dear user,'\n'a new product named $TITLE has been just published to the Data Catalogue group $GROUP by $USER_FULLNAME.";
private String groupTitle;
private String datasetId;
private String username;
private String datasetTitle;
private String userFullName;
private DataCatalogue catalogue;
private String organization;
private List<GroupBean> groups;
private HttpServletRequest request;
/**
* @param list
* @param groupTitle
* @param datasetId
* @param username
* @param catalogue
*/
public AssociationToGroupAndNotifyThread(List<GroupBean> groups, String groupTitle, String datasetId, String datasetTitle, String userFullName,
String username, DataCatalogue catalogue, String organization, HttpServletRequest request) {
this.request = request;
this.groups = groups;
this.groupTitle = groupTitle;
this.datasetId = datasetId;
this.username = username;
this.catalogue = catalogue;
this.organization = organization;
this.datasetTitle = datasetTitle;
this.userFullName = userFullName;
}
@Override
public void run() {
logger.info("Association thread started to put the dataset with id = "+ datasetId + " into group with title " + groupTitle + " for user " + username);
// create the group
CkanGroup group = catalogue.createGroup(groupTitle, groupTitle, "");
if(group == null){
logger.warn("The group doesn't exist! Unable to perform such association");
}else{
logger.debug("Group exists, going to add the user " + username + " as its admin...");
// retrieve the role to be assigned according the one the user has into the organization of the dataset
RolesCkanGroupOrOrg role = RolesCkanGroupOrOrg.valueOf(catalogue.getRoleOfUserInOrganization(username, organization, catalogue.getApiKeyFromUsername(username)).toUpperCase());
if(!role.equals(RolesCkanGroupOrOrg.ADMIN))
role = RolesCkanGroupOrOrg.MEMBER; // decrease the role to member if it is not an admin
boolean assigned = catalogue.checkRoleIntoGroup(username, groupTitle, role);
if(assigned){
logger.debug("Admin/editor role was assigned for this group, going to associate the product to the group");
boolean putIntoGroup = catalogue.assignDatasetToGroup(groupTitle, datasetId, catalogue.getApiKeyFromUsername(username));
logger.info("Was product put into group? " + putIntoGroup);
if(putIntoGroup)
notifyGroupAdmins(catalogue, groupTitle, username);
}
}
logger.info("Other groups to which the product should be associate are " + groups);
for (GroupBean groupBean : groups) {
boolean putIntoGroup = catalogue.assignDatasetToGroup(groupBean.getGroupTitle(), datasetId, catalogue.getApiKeyFromUsername(username));
logger.info("Was product put into group" + groupBean.getGroupTitle() + "? " + putIntoGroup);
if(putIntoGroup)
notifyGroupAdmins(catalogue, groupBean.getGroupTitle(), username);
}
}
/**
* Send a notification to the group admin(s) about the just added product
* @param username
* @param groupTitle
* @param catalogue
*/
private void notifyGroupAdmins(DataCatalogue catalogue, String groupTitle, String username){
// get the groups admin
Map<RolesCkanGroupOrOrg, List<String>> userAndRoles = catalogue.getRolesAndUsersGroup(groupTitle);
if(userAndRoles.containsKey(RolesCkanGroupOrOrg.ADMIN)){
List<String> admins = userAndRoles.get(RolesCkanGroupOrOrg.ADMIN);
List<String> adminsEmails = new ArrayList<String>();
for(int i = 0; i < admins.size(); i++){
String convertedName = UtilMethods.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);
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();
Iterator<String> adminIt = admins.iterator();
while (adminIt.hasNext()) {
String admin = (String) adminIt.next();
try{
adminsEmails.add(um.getUserByUsername(admin).getEmail());
}catch(Exception e){
logger.error("User with username " + admin + " doesn't exist in Liferay");
adminIt.remove();
}
}
logger.info("The list of admins for group " + groupTitle + " is " + admins);
if(admins.isEmpty())
return;
// send the email
EmailNotification mailToSend = new EmailNotification(
adminsEmails,
PRODUCT_ASSOCIATED_TO_GROUP_SUBJECT.replace("$TITLE", datasetTitle).replace("$GROUP", groupTitle),
PRODUCT_ASSOCIATED_TO_GROUP_BODY.replace("$TITLE", datasetTitle).replace("$GROUP", groupTitle).replace("$USER_FULLNAME", userFullName),
request);
mailToSend.sendEmail();
}else
logger.warn("It seems there is no user with role Admin in group " + groupTitle);
}
}

View File

@ -1,12 +1,8 @@
package org.gcube.portlets.widgets.ckandatapublisherwidget.server; package org.gcube.portlets.widgets.ckandatapublisherwidget.server.threads;
import static org.gcube.common.authorization.client.Constants.authorizationService;
import java.util.ArrayList;
import java.util.List; import java.util.List;
import org.gcube.common.authorization.library.provider.SecurityTokenProvider; 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.common.scope.api.ScopeProvider;
import org.gcube.portlets.widgets.ckandatapublisherwidget.server.utils.Utils; import org.gcube.portlets.widgets.ckandatapublisherwidget.server.utils.Utils;
import org.slf4j.Logger; import org.slf4j.Logger;
@ -58,7 +54,12 @@ public class WritePostCatalogueManagerThread extends Thread {
try{ try{
// evaluate user's token for this scope // evaluate user's token for this scope
String token = authorizationService().generateUserToken(new UserInfo(username, new ArrayList<String>()), scope); String token = Utils.tryGetElseCreateToken(username, scope);
if(token == null){
logger.warn("Unable to proceed, user's token is not available");
return;
}
logger.info("Started request to write application post " logger.info("Started request to write application post "
+ "for new product created. Scope is " + scope + " and " + "for new product created. Scope is " + scope + " and "
@ -80,10 +81,8 @@ public class WritePostCatalogueManagerThread extends Thread {
}catch(Exception e){ }catch(Exception e){
logger.error("Failed to write the post because of the following error ", e); logger.error("Failed to write the post because of the following error ", e);
}finally{ }finally{
// remove token and scope
SecurityTokenProvider.instance.reset(); SecurityTokenProvider.instance.reset();
ScopeProvider.instance.reset(); ScopeProvider.instance.reset();
} }
} }
}
}

View File

@ -1,5 +1,7 @@
package org.gcube.portlets.widgets.ckandatapublisherwidget.server.utils; package org.gcube.portlets.widgets.ckandatapublisherwidget.server.utils;
import static org.gcube.common.authorization.client.Constants.authorizationService;
import java.io.BufferedReader; import java.io.BufferedReader;
import java.io.InputStream; import java.io.InputStream;
import java.io.InputStreamReader; import java.io.InputStreamReader;
@ -11,7 +13,9 @@ import java.util.Map;
import javax.servlet.http.HttpSession; import javax.servlet.http.HttpSession;
import org.gcube.application.framework.core.session.ASLSession; import org.gcube.application.framework.core.session.ASLSession;
import org.gcube.common.authorization.client.exceptions.ObjectNotFound;
import org.gcube.common.authorization.library.provider.SecurityTokenProvider; import org.gcube.common.authorization.library.provider.SecurityTokenProvider;
import org.gcube.common.authorization.library.provider.UserInfo;
import org.gcube.common.homelibrary.home.exceptions.InternalErrorException; import org.gcube.common.homelibrary.home.exceptions.InternalErrorException;
import org.gcube.common.homelibrary.home.workspace.WorkspaceItem; import org.gcube.common.homelibrary.home.workspace.WorkspaceItem;
import org.gcube.common.homelibrary.home.workspace.folder.items.GCubeItem; import org.gcube.common.homelibrary.home.workspace.folder.items.GCubeItem;
@ -479,4 +483,28 @@ public class Utils {
return toReturn; return toReturn;
} }
/**
* First check to retrieve the token, else create it
* @param username
* @param context
* @return the user token for the context
*/
public static String tryGetElseCreateToken(String username, String context) {
String token = null;
try{
try{
logger.debug("Checking if token for user " + username + " in context " + context + " already exists...");
token = authorizationService().resolveTokenByUserAndContext(username, context);
logger.debug("It exists!");
}catch(ObjectNotFound e){
logger.info("Creating token for user " + username + " and context " + context);
token = authorizationService().generateUserToken(new UserInfo(username, new ArrayList<String>()), context);
logger.debug("received token: "+ token.substring(0, 5) + "***********************");
}
}catch(Exception e){
logger.error("Failed both token retrieval and creation", e);
}
return token;
}
} }

View File

@ -180,7 +180,7 @@ public class WorkspaceUtils {
} }
/** /**
* Replaces the "/" char with a custom one * Replaces the "/" char with a custom one and return an editable name for the user
* @param rootElem * @param rootElem
* @param pathSeparatorInWs * @param pathSeparatorInWs
*/ */

View File

@ -41,5 +41,11 @@ public class GroupBean implements Serializable {
public void setGroupName(String groupName) { public void setGroupName(String groupName) {
this.groupName = groupName; this.groupName = groupName;
} }
@Override
public String toString() {
return "GroupBean [groupTitle=" + groupTitle + ", groupName="
+ groupName + "]";
}
} }