diff --git a/.classpath b/.classpath index 6d6f9dd..4c6e618 100644 --- a/.classpath +++ b/.classpath @@ -12,7 +12,7 @@ - + @@ -23,7 +23,7 @@ - + diff --git a/pom.xml b/pom.xml index a2b7261..701bd10 100644 --- a/pom.xml +++ b/pom.xml @@ -83,10 +83,6 @@ portal-manager provided - - org.gcube.portal - notifications-common-library - org.gcube.portlets.user gcube-url-shortener @@ -121,18 +117,18 @@ org.gcube.core common-scope-maps - provided + compile org.gcube.core common-encryption - provided + compile org.gcube.common authorization-client - provided + compile junit diff --git a/src/main/java/org/gcube/datacatalogue/grsf_manage_widget/client/GRSFManageWidget.java b/src/main/java/org/gcube/datacatalogue/grsf_manage_widget/client/GRSFManageWidget.java index c9c6d1d..2ffa8ab 100644 --- a/src/main/java/org/gcube/datacatalogue/grsf_manage_widget/client/GRSFManageWidget.java +++ b/src/main/java/org/gcube/datacatalogue/grsf_manage_widget/client/GRSFManageWidget.java @@ -12,6 +12,8 @@ public class GRSFManageWidget implements EntryPoint { */ public void onModuleLoad(){ // HandlerManager eventBus = new HandlerManager(null); - // RootPanel.get("manageDiv").add(new ManageProductWidget("fffb6167-b570-42a8-92b9-5be28549c3b8", eventBus)); + //RootPanel.get("manageDiv").add(new ManageProductWidget("fffb6167-b570-42a8-92b9-5be28549c3b8", eventBus)); + // RootPanel.get("manageDiv").add(new ManageRevertOperationWidget( + // "random-url-here", eventBus)); } } diff --git a/src/main/java/org/gcube/datacatalogue/grsf_manage_widget/client/GRSFManageWidgetService.java b/src/main/java/org/gcube/datacatalogue/grsf_manage_widget/client/GRSFManageWidgetService.java index 9b93aa8..85c5e7d 100644 --- a/src/main/java/org/gcube/datacatalogue/grsf_manage_widget/client/GRSFManageWidgetService.java +++ b/src/main/java/org/gcube/datacatalogue/grsf_manage_widget/client/GRSFManageWidgetService.java @@ -1,6 +1,7 @@ package org.gcube.datacatalogue.grsf_manage_widget.client; import org.gcube.datacatalogue.grsf_manage_widget.shared.ManageProductBean; +import org.gcube.datacatalogue.grsf_manage_widget.shared.RevertableOperationInfo; import com.google.gwt.user.client.rpc.RemoteService; import com.google.gwt.user.client.rpc.RemoteServiceRelativePath; @@ -45,10 +46,18 @@ public interface GRSFManageWidgetService extends RemoteService { String checkIdentifierExistsInDomain(String id, String domain) throws Exception; /** - * Check if the given url for reverting the operation is valid and send the request to the knowledge base + * Check if the given url for reverting the operation is valid and get back the needed info to proceed * @param url * @throws Exception */ - void validateRevertOperation(String url) throws Exception; + RevertableOperationInfo validateRevertOperation(String url) throws Exception; + + /** + * Perform a revert operation + * @param rInfo + * @return + * @throws Exception + */ + Boolean performRevertOperation(RevertableOperationInfo rInfo) throws Exception; } diff --git a/src/main/java/org/gcube/datacatalogue/grsf_manage_widget/client/GRSFManageWidgetServiceAsync.java b/src/main/java/org/gcube/datacatalogue/grsf_manage_widget/client/GRSFManageWidgetServiceAsync.java index 481cffd..8d402e8 100644 --- a/src/main/java/org/gcube/datacatalogue/grsf_manage_widget/client/GRSFManageWidgetServiceAsync.java +++ b/src/main/java/org/gcube/datacatalogue/grsf_manage_widget/client/GRSFManageWidgetServiceAsync.java @@ -4,6 +4,7 @@ package org.gcube.datacatalogue.grsf_manage_widget.client; import org.gcube.datacatalogue.grsf_manage_widget.shared.ManageProductBean; +import org.gcube.datacatalogue.grsf_manage_widget.shared.RevertableOperationInfo; import com.google.gwt.user.client.rpc.AsyncCallback; @@ -27,6 +28,9 @@ public interface GRSFManageWidgetServiceAsync { void checkIdentifierExistsInDomain(String id, String domain, AsyncCallback callback); - void validateRevertOperation(String url, AsyncCallback callback); + void validateRevertOperation(String url, AsyncCallback callback); + + void performRevertOperation(RevertableOperationInfo rInfo, + AsyncCallback callback); } diff --git a/src/main/java/org/gcube/datacatalogue/grsf_manage_widget/client/view/ManageProductWidget.java b/src/main/java/org/gcube/datacatalogue/grsf_manage_widget/client/view/ManageProductWidget.java index c0c0e8c..95f8730 100644 --- a/src/main/java/org/gcube/datacatalogue/grsf_manage_widget/client/view/ManageProductWidget.java +++ b/src/main/java/org/gcube/datacatalogue/grsf_manage_widget/client/view/ManageProductWidget.java @@ -52,8 +52,7 @@ public class ManageProductWidget extends Composite{ private static GRSFManageWidgetServiceAsync service = GWT.create(GRSFManageWidgetService.class); - private static ManageProductWidgetUiBinder uiBinder = GWT - .create(ManageProductWidgetUiBinder.class); + private static ManageProductWidgetUiBinder uiBinder = GWT.create(ManageProductWidgetUiBinder.class); interface ManageProductWidgetUiBinder extends UiBinder { @@ -174,7 +173,7 @@ public class ManageProductWidget extends Composite{ // show modal manageProductModal.addStyleName("management-metadata-modal-style"); - // manageProductModal.getElement().getStyle().setWidth(60, Unit.PCT); + manageProductModal.getElement().getStyle().setWidth(60, Unit.PCT); manageProductModal.show(); // async request to fetch the product @@ -331,11 +330,19 @@ public class ManageProductWidget extends Composite{ if(similarRecordPanel != null) bean.setSimilarGrsfRecords(similarRecordPanel.getSimilarRecords()); else - bean.setSimilarGrsfRecords(new ArrayList(0)); + bean.setSimilarGrsfRecords(new ArrayList(0)); // add the suggested ones, if any bean.getSimilarGrsfRecords().addAll(suggestedMergesPanel.getSimilarRecords()); + // set the merge operator on the bean if there is at least one merge to be done + for(SimilarGRSFRecord sR: bean.getSimilarGrsfRecords()){ + if(sR.isSuggestedMerge()){ + bean.setMergesInvolved(true); + break; + } + } + // set new values bean.setAnnotation(new HTML(annotationArea.getText().trim()).getText()); diff --git a/src/main/java/org/gcube/datacatalogue/grsf_manage_widget/client/view/ManageRevertOperationWidget.java b/src/main/java/org/gcube/datacatalogue/grsf_manage_widget/client/view/ManageRevertOperationWidget.java new file mode 100644 index 0000000..50f28a6 --- /dev/null +++ b/src/main/java/org/gcube/datacatalogue/grsf_manage_widget/client/view/ManageRevertOperationWidget.java @@ -0,0 +1,198 @@ +package org.gcube.datacatalogue.grsf_manage_widget.client.view; + +import java.util.Date; + +import org.gcube.datacatalogue.grsf_manage_widget.client.GRSFManageWidgetService; +import org.gcube.datacatalogue.grsf_manage_widget.client.GRSFManageWidgetServiceAsync; +import org.gcube.datacatalogue.grsf_manage_widget.shared.RevertableOperationInfo; + +import com.github.gwtbootstrap.client.ui.AlertBlock; +import com.github.gwtbootstrap.client.ui.Button; +import com.github.gwtbootstrap.client.ui.Icon; +import com.github.gwtbootstrap.client.ui.Image; +import com.github.gwtbootstrap.client.ui.Modal; +import com.github.gwtbootstrap.client.ui.TextArea; +import com.github.gwtbootstrap.client.ui.constants.AlertType; +import com.google.gwt.core.client.GWT; +import com.google.gwt.dom.client.Style.FontWeight; +import com.google.gwt.dom.client.Style.Unit; +import com.google.gwt.event.dom.client.ClickEvent; +import com.google.gwt.event.shared.HandlerManager; +import com.google.gwt.i18n.client.DateTimeFormat; +import com.google.gwt.uibinder.client.UiBinder; +import com.google.gwt.uibinder.client.UiField; +import com.google.gwt.uibinder.client.UiHandler; +import com.google.gwt.user.client.rpc.AsyncCallback; +import com.google.gwt.user.client.ui.Anchor; +import com.google.gwt.user.client.ui.Composite; +import com.google.gwt.user.client.ui.VerticalPanel; +import com.google.gwt.user.client.ui.Widget; + +public class ManageRevertOperationWidget extends Composite { + + private static ManageRevertOperationWidgetUiBinder uiBinder = GWT + .create(ManageRevertOperationWidgetUiBinder.class); + + interface ManageRevertOperationWidgetUiBinder extends + UiBinder { + } + + public ManageRevertOperationWidget() { + initWidget(uiBinder.createAndBindUi(this)); + } + + @UiField + VerticalPanel moreInfoAboutOperation; + + @UiField + Modal revertOperationModal; + + @UiField + Icon loaderIcon; + + @UiField + Image loadingImage; + + @UiField + AlertBlock infoBlock; + + @UiField + Button revertButton; + + @UiField + Button cancelButton; + + @UiField + TextArea requestAuthor; + + @UiField + TextArea requestTypeBox; + + @UiField + TextArea requestRecordUUID; + + @UiField + TextArea requestTimestamp; + + private static GRSFManageWidgetServiceAsync service = GWT.create(GRSFManageWidgetService.class); + + private HandlerManager eventBus; + public static final String LOADING_IMAGE_URL = GWT.getModuleBaseURL() + "../images/loader.gif"; + private RevertableOperationInfo revertableOperation = null; + + public ManageRevertOperationWidget(String encryptedUrlOperation, HandlerManager eventBus) { + initWidget(uiBinder.createAndBindUi(this)); + + this.eventBus = eventBus; + + GWT.log("Encrypted url is " + encryptedUrlOperation); + + if(encryptedUrlOperation == null || encryptedUrlOperation.isEmpty()) + return; + + // start loader service + loadingImage.setUrl(LOADING_IMAGE_URL); + loadingImage.setVisible(true); + + // show modal + revertOperationModal.addStyleName("management-metadata-modal-style"); + // revertOperationModal.getElement().getStyle().setWidth(60, Unit.PCT); + revertOperationModal.show(); + + // async request to fetch the product + loadModalContent(encryptedUrlOperation); + + } + + /** + * Validate the parameters of the url and ask the editor/reviewer what he/she wants to do. + * @author Costantino Perciante at ISTI-CNR (costantino.perciante@isti.cnr.it) + * @param encryptedUrlOperation + */ + private void loadModalContent(String encryptedUrlOperation) { + + revertButton.setEnabled(false); + + service.validateRevertOperation(encryptedUrlOperation, new AsyncCallback() { + + @Override + public void onSuccess(RevertableOperationInfo result) { + + loadingImage.setVisible(false); + + if(result != null){ + revertableOperation = result; + String dateString = DateTimeFormat.getFormat("HH:mm:ss").format(new Date(revertableOperation.getTimestamp())); + requestAuthor.setText(revertableOperation.getAdmin()); + requestTypeBox.setText(revertableOperation.getOperation().toString().toUpperCase()); + requestRecordUUID.setText(revertableOperation.getUuid()); + requestTimestamp.setText(dateString); + + Anchor viewRecord = new Anchor(); + viewRecord.setText("View Record"); + viewRecord.getElement().getStyle().setFontWeight(FontWeight.BOLD); + viewRecord.setHref(revertableOperation.getRecordUrl()); + viewRecord.setTarget("_blank"); + viewRecord.getElement().getStyle().setMarginBottom(20, Unit.PX); + moreInfoAboutOperation.add(viewRecord); + moreInfoAboutOperation.setVisible(true); + revertButton.setEnabled(true); + }else + displayError(null); + } + + @Override + public void onFailure(Throwable caught) { + loadingImage.setVisible(false); + displayError(caught); + } + }); + + } + + @UiHandler("revertButton") + void onSaveButton(ClickEvent ce){ + + loaderIcon.setVisible(true); + revertButton.setEnabled(false); + + service.performRevertOperation(revertableOperation, new AsyncCallback() { + + @Override + public void onSuccess(Boolean result) { + + revertButton.setEnabled(true); + loaderIcon.setVisible(false); + + if(!result) + displayError(null); + else{ + infoBlock.setVisible(true); + infoBlock.setType(AlertType.SUCCESS); + infoBlock.setText("The request has been processed successfully!"); + revertButton.removeFromParent(); + cancelButton.removeFromParent(); + } + } + + @Override + public void onFailure(Throwable caught) { + displayError(caught); + } + }); + + } + + @UiHandler("cancelButton") + void onCancelButton(ClickEvent ce){ + revertOperationModal.hide(); + } + + protected void displayError(Throwable caught) { + + infoBlock.setVisible(true); + infoBlock.setType(AlertType.ERROR); + infoBlock.setText("Unable to perform this operation. " + (caught != null ? "Error was " + caught : "")); + + } +} diff --git a/src/main/java/org/gcube/datacatalogue/grsf_manage_widget/client/view/ManageRevertOperationWidget.ui.xml b/src/main/java/org/gcube/datacatalogue/grsf_manage_widget/client/view/ManageRevertOperationWidget.ui.xml new file mode 100644 index 0000000..ea0db89 --- /dev/null +++ b/src/main/java/org/gcube/datacatalogue/grsf_manage_widget/client/view/ManageRevertOperationWidget.ui.xml @@ -0,0 +1,87 @@ + + + + .loader-image { + display: block; + margin: auto auto; + } + + + + + + + + + + + + + + + + + + Request Type: + + + + + + + + + Request was performed by: + + + + + + + + + Request was performed on Record: + + + + + + + + + Request was performed at: + + + + + + + + + + + + + Cancel + Revert + + + + \ No newline at end of file diff --git a/src/main/java/org/gcube/datacatalogue/grsf_manage_widget/client/view/subwidgets/ConnectToWidget.java b/src/main/java/org/gcube/datacatalogue/grsf_manage_widget/client/view/subwidgets/ConnectToWidget.java index bf12942..508a2a1 100644 --- a/src/main/java/org/gcube/datacatalogue/grsf_manage_widget/client/view/subwidgets/ConnectToWidget.java +++ b/src/main/java/org/gcube/datacatalogue/grsf_manage_widget/client/view/subwidgets/ConnectToWidget.java @@ -234,7 +234,7 @@ public class ConnectToWidget extends Composite{ } }); box.setWidth("512px"); - box.setPlaceholder("Insert the Identifier (UUID) of the record to connect and press ENTER"); + box.setPlaceholder("Copy and Paste the Identifier (UUID) of the record to connect here"); vpLeft.add(semanticIdentifier); vpLeft.add(box); @@ -276,6 +276,9 @@ public class ConnectToWidget extends Composite{ protected void validateUUID(final TextBox box, final ConnectedBean c, final Icon icon, final Anchor view, final String acceptedDomain) { + if(!box.isEnabled()) + return; + final String currentText = box.getText().trim(); c.setKnowledgeBaseId(null); c.setConnect(false); @@ -306,7 +309,6 @@ public class ConnectToWidget extends Composite{ public void onSuccess(String result) { icon.setSpin(false); - if(result != null){ c.setKnowledgeBaseId(currentText); c.setConnect(true); @@ -314,15 +316,15 @@ public class ConnectToWidget extends Composite{ icon.setTitle("Accepted"); view.setHref(result); view.setVisible(true); + box.setEnabled(false); } else{ icon.setType(IconType.BAN_CIRCLE); icon.setTitle("Not a valid UUID"); view.setVisible(false); + box.setEnabled(true); } - icon.setVisible(true); - box.setEnabled(true); } @Override diff --git a/src/main/java/org/gcube/datacatalogue/grsf_manage_widget/client/view/subwidgets/SuggestMerges.java b/src/main/java/org/gcube/datacatalogue/grsf_manage_widget/client/view/subwidgets/SuggestMerges.java index fa8696f..118ee2e 100644 --- a/src/main/java/org/gcube/datacatalogue/grsf_manage_widget/client/view/subwidgets/SuggestMerges.java +++ b/src/main/java/org/gcube/datacatalogue/grsf_manage_widget/client/view/subwidgets/SuggestMerges.java @@ -14,8 +14,6 @@ import com.github.gwtbootstrap.client.ui.TextBox; import com.github.gwtbootstrap.client.ui.constants.ButtonType; import com.github.gwtbootstrap.client.ui.constants.IconType; import com.google.gwt.core.client.GWT; -import com.google.gwt.core.client.Scheduler; -import com.google.gwt.core.client.Scheduler.ScheduledCommand; import com.google.gwt.dom.client.Style.Float; import com.google.gwt.dom.client.Style.FontWeight; import com.google.gwt.dom.client.Style.Unit; @@ -112,21 +110,14 @@ public class SuggestMerges extends Composite { // add a couple of handlers box.addKeyPressHandler(new KeyPressHandler() { - + @Override public void onKeyPress(KeyPressEvent event) { - GWT.log("onKeyPress " + event.getNativeEvent().getKeyCode()); - Scheduler.get().scheduleDeferred(new ScheduledCommand() { - - @Override - public void execute() { - box.setFocus(false); - } - }); + validateUUID(box, s, icon, view); + } }); box.addChangeHandler(new ChangeHandler() { - @Override public void onChange(ChangeEvent event) { GWT.log("onChange"); @@ -190,6 +181,9 @@ public class SuggestMerges extends Composite { */ protected void validateUUID(final TextBox box, final SimilarGRSFRecord s, final Icon icon, final Anchor view) { + if(!box.isEnabled()) + return; + final String currentText = box.getText().trim(); s.setKnowledgeBaseId(null); s.setSuggestedMerge(false); @@ -199,7 +193,7 @@ public class SuggestMerges extends Composite { if(currentText == null || currentText.isEmpty()) return; - + if(!currentText.matches(REGEX_UUID)){ icon.setType(IconType.BAN_CIRCLE); icon.setTitle("Not a valid UUID"); @@ -220,7 +214,7 @@ public class SuggestMerges extends Composite { @Override public void onSuccess(String result) { icon.setSpin(false); - + if(result != null){ s.setKnowledgeBaseId(currentText); s.setSuggestedMerge(true); @@ -228,14 +222,15 @@ public class SuggestMerges extends Composite { icon.setTitle("Accepted"); view.setHref(result); view.setVisible(true); + box.setEnabled(false); } else{ icon.setType(IconType.BAN_CIRCLE); icon.setTitle("Not a valid UUID"); view.setVisible(false); + box.setEnabled(true); } icon.setVisible(true); - box.setEnabled(true); } @Override diff --git a/src/main/java/org/gcube/datacatalogue/grsf_manage_widget/server/manage/GRSFNotificationService.java b/src/main/java/org/gcube/datacatalogue/grsf_manage_widget/server/manage/GRSFNotificationService.java index 795f4a1..8ea85b0 100644 --- a/src/main/java/org/gcube/datacatalogue/grsf_manage_widget/server/manage/GRSFNotificationService.java +++ b/src/main/java/org/gcube/datacatalogue/grsf_manage_widget/server/manage/GRSFNotificationService.java @@ -21,6 +21,8 @@ import org.gcube.datacatalogue.common.enums.Status; import org.gcube.datacatalogue.grsf_manage_widget.client.GRSFManageWidgetService; import org.gcube.datacatalogue.grsf_manage_widget.shared.ConnectedBean; import org.gcube.datacatalogue.grsf_manage_widget.shared.ManageProductBean; +import org.gcube.datacatalogue.grsf_manage_widget.shared.RevertableOperationInfo; +import org.gcube.datacatalogue.grsf_manage_widget.shared.RevertableOperations; import org.gcube.datacatalogue.grsf_manage_widget.shared.SimilarGRSFRecord; import org.gcube.datacatalogue.grsf_manage_widget.shared.SourceRecord; import org.gcube.datacatalogue.grsf_manage_widget.shared.ex.NoGRSFRecordException; @@ -372,7 +374,16 @@ public class GRSFNotificationService extends RemoteServiceServlet implements GRS } @Override - public void validateRevertOperation(String encryptedUrl) throws Exception { + public RevertableOperationInfo validateRevertOperation(String encryptedUrl) throws Exception { + + if(!Utils.isIntoPortal()){ + String baseUrl = "url of the record here"; + String fullName = "Andrea Rossi"; + String uuid = UUID.randomUUID().toString(); + String adminInUrl = "costantino.perciante"; + long timestamp = System.currentTimeMillis() - 1000 * ((long)(Math.random() * 10 * 60 * 60)); + return new RevertableOperationInfo(baseUrl, fullName, uuid, adminInUrl, timestamp, RevertableOperations.MERGE); + } PortalContext pContext = PortalContext.getConfiguration(); String context = Utils.getScopeFromClientUrl(getThreadLocalRequest()); @@ -402,35 +413,69 @@ public class GRSFNotificationService extends RemoteServiceServlet implements GRS if(!isValidTimestamp) throw new Exception("This operation can no longer be reverted (link expired)!"); - - String keyPerContext = UtilMethods.concatenateSessionKeyScope(Constants.GRSF_UPDATER_SERVICE, context); - String baseUrl = (String)getThreadLocalRequest().getSession().getAttribute(keyPerContext); - if(baseUrl == null || baseUrl.isEmpty()){ - baseUrl = GRSFUpdaterServiceClient.discoverEndPoint(context); - getThreadLocalRequest().getSession().setAttribute(keyPerContext, baseUrl); - } - - if(baseUrl == null || baseUrl.isEmpty()) - throw new Exception("Unable to discover grsf-updater service!"); + + DataCatalogue catalogue = getCatalogue(context); + String recordUrl = catalogue.getDataset(uuid, catalogue.getApiKeyFromUsername(username)).getExtrasAsHashMap().get(Constants.ITEM_URL_FIELD); // check if it is a reviewer, than he can do what he wants (no matter the admin) - try(CloseableHttpClient httpClient = HttpClientBuilder.create().build();){ - if(isReviewer){ - GRSFUpdaterServiceClient.revertOperation(httpClient, baseUrl, fullName, uuid); - }else{ + if(isReviewer){ + return new RevertableOperationInfo(recordUrl, + fullName, uuid, adminInUrl, decryptedUrl.getTimestamp(), decryptedUrl.getOperation()); + }else{ - if(!username.equals(adminInUrl)) - throw new Exception("You are not the editor allowed to perform this operation!"); - else - GRSFUpdaterServiceClient.revertOperation(httpClient, baseUrl, fullName, uuid); - } - }catch(Exception e){ - logger.error("Unable to update this Item ", e); - throw e; + if(!username.equals(adminInUrl)) + throw new Exception("You are not the editor allowed to perform this operation!"); + else + return new RevertableOperationInfo(recordUrl, + fullName, uuid, adminInUrl, decryptedUrl.getTimestamp(), decryptedUrl.getOperation()); } } + @Override + public Boolean performRevertOperation(RevertableOperationInfo rInfo) + throws Exception { + + if(!Utils.isIntoPortal()){ + // random result + boolean toReturn = Math.random() > 0.5; + + if(toReturn){ + + boolean throwException = Math.random() > 0.5; + if(throwException) + throw new Exception("Unable to execute request for XYZ"); + + } + return toReturn; + } + + HttpServletRequest threadRequest = getThreadLocalRequest(); + String context = Utils.getScopeFromClientUrl(threadRequest); + String token = SecurityTokenProvider.instance.get(); + + try(CloseableHttpClient httpClient = HttpClientBuilder.create().build();){ + + String keyPerContext = UtilMethods.concatenateSessionKeyScope(Constants.GRSF_UPDATER_SERVICE, context); + String baseUrl = (String)getThreadLocalRequest().getSession().getAttribute(keyPerContext); + if(baseUrl == null || baseUrl.isEmpty()){ + baseUrl = GRSFUpdaterServiceClient.discoverEndPoint(context); + getThreadLocalRequest().getSession().setAttribute(keyPerContext, baseUrl); + } + + if(baseUrl == null || baseUrl.isEmpty()) + throw new Exception("Unable to discover grsf-updater service!"); + + Utils.revertOperation(httpClient, baseUrl, rInfo, token, context, PortalContext.getConfiguration().getCurrentGroupId(threadRequest)); + + } + catch(Exception e){ + logger.error("Unable to revert operation ", e); + throw e; + } + return true; + } + /** * Check if the current user is an editor * @param username @@ -484,21 +529,21 @@ public class GRSFNotificationService extends RemoteServiceServlet implements GRS @Override public String checkIdentifierExistsInDomain(String id, String domain) throws Exception { - + if(!Utils.isIntoPortal()){ - + boolean throwException = Math.random() > 0.5; - + // simulate some delay... Thread.sleep(2500); - + if(throwException) throw new Exception("The suggested record is not a GRSF record"); - + return "http://data.d4science.org/catalogue/grsf_admin/" + id; - + } - + String scopePerCurrentUrl = Utils.getScopeFromClientUrl(getThreadLocalRequest()); DataCatalogue catalogue = getCatalogue(scopePerCurrentUrl); String username = Utils.getCurrentUser(getThreadLocalRequest()).getUsername(); diff --git a/src/main/java/org/gcube/datacatalogue/grsf_manage_widget/server/manage/RevertOperationUrl.java b/src/main/java/org/gcube/datacatalogue/grsf_manage_widget/server/manage/RevertOperationUrl.java index 4b20e0b..33013aa 100644 --- a/src/main/java/org/gcube/datacatalogue/grsf_manage_widget/server/manage/RevertOperationUrl.java +++ b/src/main/java/org/gcube/datacatalogue/grsf_manage_widget/server/manage/RevertOperationUrl.java @@ -4,6 +4,7 @@ import java.net.URLDecoder; import java.net.URLEncoder; import org.gcube.common.encryption.StringEncrypter; +import org.gcube.datacatalogue.grsf_manage_widget.shared.RevertableOperations; import org.gcube.portlets.user.urlshortener.UrlShortener; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -24,32 +25,11 @@ public class RevertOperationUrl { public static final String OPERATION_REVERT_QUERY_PARAM = "operation_revert"; public static final long TTL = 1000 * 60 * 60 * 24; - /** - * For now only Merge can be reverted - * @author Costantino Perciante at ISTI-CNR (costantino.perciante@isti.cnr.it) - */ - public enum Operation { - - MERGE("merge"); - // DISSECT("dissect"); - private String name; - - private Operation(String name) { - this.name = name; - } - - @Override - public String toString() { - return name; - } - - } - private String baseUrl; private String admin; private long timestamp; private String uuid; - private Operation operation; + private RevertableOperations operation; /** * @param admin @@ -59,7 +39,7 @@ public class RevertOperationUrl { * @param op */ public RevertOperationUrl(String baseUrl, String admin, long timestamp, String uuid, - Operation operation) { + RevertableOperations operation) { super(); this.baseUrl = baseUrl; this.admin = admin; @@ -133,7 +113,7 @@ public class RevertOperationUrl { this.uuid = value; break; case OPERATION_REVERT_QUERY_PARAM: - this.operation = Operation.valueOf(value); + this.operation = RevertableOperations.valueOf(value); break; default: break; @@ -189,12 +169,12 @@ public class RevertOperationUrl { } - public Operation getOperation() { + public RevertableOperations getOperation() { return operation; } - public void setOperation(Operation operation) { + public void setOperation(RevertableOperations operation) { this.operation = operation; } diff --git a/src/main/java/org/gcube/datacatalogue/grsf_manage_widget/server/manage/SocialCommunications.java b/src/main/java/org/gcube/datacatalogue/grsf_manage_widget/server/manage/SocialCommunications.java index 293d602..87cfe3f 100644 --- a/src/main/java/org/gcube/datacatalogue/grsf_manage_widget/server/manage/SocialCommunications.java +++ b/src/main/java/org/gcube/datacatalogue/grsf_manage_widget/server/manage/SocialCommunications.java @@ -15,8 +15,9 @@ import org.gcube.common.resources.gcore.GCoreEndpoint; import org.gcube.common.scope.api.ScopeProvider; import org.gcube.datacatalogue.ckanutillibrary.server.DataCatalogue; import org.gcube.datacatalogue.common.Constants; -import org.gcube.datacatalogue.grsf_manage_widget.server.manage.RevertOperationUrl.Operation; import org.gcube.datacatalogue.grsf_manage_widget.shared.ManageProductBean; +import org.gcube.datacatalogue.grsf_manage_widget.shared.RevertableOperationInfo; +import org.gcube.datacatalogue.grsf_manage_widget.shared.RevertableOperations; import org.gcube.resources.discovery.client.api.DiscoveryClient; import org.gcube.resources.discovery.client.queries.api.SimpleQuery; import org.gcube.vomanagement.usermanagement.RoleManager; @@ -78,6 +79,13 @@ public class SocialCommunications { private static final String REVERT_LINK_PIECE = "
The request involves a merge operation. You can reject the merge by exploiting this link LINK in the following 24 hours."; + // on revert operation + private static final String EMAIL_REVIEWER_REVERT = "Dear GRSF Reviewer," + + "
a revert operation (undo merge) has been requested on record RECORD_URL, by USERNAME."; + + private static final String EMAIL_EDITOR_REVERT = "Dear USER_FULLNAME," + +"
a revert operation (undo merge) has been requested on this RECORD_URL you managed."; + /** * * @param context @@ -128,9 +136,10 @@ public class SocialCommunications { * @param fullName * @param hashtags * @param enablePostNotification + * @throws Exception */ @SuppressWarnings("unchecked") - public static void writeProductPost(ManageProductBean bean, String username, String fullName, String report, boolean enablePostNotification){ + public static void writeProductPost(ManageProductBean bean, String username, String fullName, String report, boolean enablePostNotification) throws Exception{ // discover service endpoint for the social networking library String currentScope = ScopeProvider.instance.get(); @@ -142,6 +151,7 @@ public class SocialCommunications { if(basePath == null){ logger.error("Unable to write a post because there is no social networking service available"); + throw new Exception("Unable to discover the social networking service"); }else{ @@ -222,7 +232,7 @@ public class SocialCommunications { * @throws Exceptio */ @SuppressWarnings("unchecked") - public static void sendEmailAdministrators( + public static void sendEmailAdministratorsOnMerge( ManageProductBean bean, DataCatalogue catalogue, String username, @@ -248,7 +258,7 @@ public class SocialCommunications { logger.info("List of " + Constants.GRSF_CATALOGUE_REVIEWER_ROLE + " is " + reviewers); // build the url that allows to revert the operation - Operation operation = Operation.MERGE; + RevertableOperations operation = RevertableOperations.MERGE; // discover service endpoint for the social networking library String currentScope = ScopeProvider.instance.get(); @@ -260,6 +270,7 @@ public class SocialCommunications { if(basePath == null){ logger.error("Unable to write a post because there is no social networking service available"); + throw new Exception("Unable to discover the social networking service"); }else{ @@ -352,7 +363,137 @@ public class SocialCommunications { } }catch(Exception e){ - logger.error("Failed to create a post", e); + logger.error("Failed to send messages", e); + } + } + + } + + public static void sendEmailAdministratorsOnOperationReverted( + RevertableOperationInfo rInfo, + long groupId + ) throws Exception { + + // get the list of GRSF Reviewers to alert them as well + RoleManager roleManager = new LiferayRoleManager(); + List teamRoles = roleManager.listTeamsByGroup(groupId); + List reviewers = new ArrayList<>(); + UserManager um = new LiferayUserManager(); + + for(GCubeTeam tr: teamRoles){ + if(tr.getTeamName().equals(Constants.GRSF_CATALOGUE_REVIEWER_ROLE)) + reviewers.add(um.getUserById(tr.getUserId()).getUsername()); + } + + // if the user is a reviewer, then send the email just once + reviewers.remove(rInfo.getAdmin()); + + logger.info("List of " + Constants.GRSF_CATALOGUE_REVIEWER_ROLE + " is " + reviewers); + + // build the url that allows to revert the operation + RevertableOperations operation = RevertableOperations.MERGE; + + // discover service endpoint for the social networking library + String currentScope = ScopeProvider.instance.get(); + String tokenUser = SecurityTokenProvider.instance.get(); + + logger.info("Current scope for writeProductPost is " + currentScope + " and token is " + tokenUser.substring(0, 10) + "***************"); + String basePath = getBaseUrlSocialService(currentScope); + + if(basePath == null){ + + logger.error("Unable to write a post because there is no social networking service available"); + throw new Exception("Unable to discover the social networking service"); + + }else{ + + basePath = basePath.endsWith("/") ? basePath : basePath + "/"; + + try(CloseableHttpClient client = HttpClientBuilder.create().setRedirectStrategy(new LaxRedirectStrategy()).build();){ + + // ask token application + HttpPost postRequest = new HttpPost(basePath + SOCIAL_SERVICE_APPLICATION_TOKEN + "?gcube-token=" + tokenUser); + StringEntity input = new StringEntity("{\"app_id\":\"" + APPLICATION_ID_CATALOGUE_MANAGER + "\"}"); + input.setContentType(MEDIATYPE_JSON); + postRequest.setEntity(input); + HttpResponse response = client.execute(postRequest); + + logger.debug("Url is " + basePath + SOCIAL_SERVICE_APPLICATION_TOKEN + "?gcube-token=" + tokenUser); + + if (response.getStatusLine().getStatusCode() != 201) { + throw new RuntimeException("Failed to retrieve application token : HTTP error code : " + + response.getStatusLine().getStatusCode()); + }else{ + + Map mapResponseGeneratedToken = getResponseEntityAsJSON(response); + boolean successGeneratedToken = (boolean)mapResponseGeneratedToken.get("success"); + if(!successGeneratedToken){ + + throw new RuntimeException("Failed to generate the token for the application!" + + " Error message is " + mapResponseGeneratedToken.get("message")); + + }else{ + + String applicationToken = (String)mapResponseGeneratedToken.get("result"); + + String messageToEditor = EMAIL_EDITOR_REVERT.replace("RECORD_URL", rInfo.getRecordUrl()).replace("USER_FULLNAME", rInfo.getFullName()); + String messageToReviewer = EMAIL_REVIEWER_REVERT.replace("USER_FULLNAME", rInfo.getFullName()).replace("RECORD_URL", rInfo.getRecordUrl()); + String subject = "Revert merge request on GRSF Record"; + + // send email to the editor + logger.info("The message that is going to be send to the editor is\n" + messageToEditor); + postRequest = new HttpPost(basePath + SOCIAL_SEND_EMAIL + "?gcube-token=" + applicationToken); + JSONObject reqMessage = new JSONObject(); + reqMessage.put("subject", subject); + reqMessage.put("body", messageToEditor); + JSONArray recipients = new JSONArray(); + JSONObject recipient = new JSONObject(); + recipient.put("id", rInfo.getAdmin()); + recipients.add(recipient); + reqMessage.put("recipients", recipients); + input = new StringEntity(reqMessage.toJSONString()); + input.setContentType(MEDIATYPE_JSON); + postRequest.setEntity(input); + response = client.execute(postRequest); + + Map mapResponseWritePost = getResponseEntityAsJSON(response); + + if (response.getStatusLine().getStatusCode() != 201){ + logger.error("Failed to send message to editor : HTTP error code : " + + response.getStatusLine().getStatusCode() + mapResponseWritePost.get("message")); + } + + // send email to the reviewers + logger.info("The message that is going to be send to the reviewers is\n" + messageToReviewer); + postRequest = new HttpPost(basePath + SOCIAL_SEND_EMAIL + "?gcube-token=" + applicationToken); + reqMessage = new JSONObject(); + reqMessage.put("subject", subject); + reqMessage.put("body", messageToReviewer); + recipients = new JSONArray(); + for(String reviewer: reviewers){ + JSONObject recip = new JSONObject(); + recip.put("id", reviewer); + recipients.add(recip); + } + reqMessage.put("recipients", recipients); + input = new StringEntity(reqMessage.toJSONString()); + input.setContentType(MEDIATYPE_JSON); + postRequest.setEntity(input); + response = client.execute(postRequest); + mapResponseWritePost = getResponseEntityAsJSON(response); + + if (response.getStatusLine().getStatusCode() != 201){ + logger.error("Failed to send message to editor : HTTP error code : " + + response.getStatusLine().getStatusCode() + mapResponseWritePost.get("message")); + } + + } + + } + + }catch(Exception e){ + logger.error("Failed to send messages", e); + throw new Exception(e); } } @@ -364,7 +505,7 @@ public class SocialCommunications { * @return * @throws Exception */ - public static String getEncodedUrlManage(Operation operation, String administrator, long timestamp, String uuid, HttpServletRequest httpServletRequest) throws Exception{ + public static String getEncodedUrlManage(RevertableOperations operation, String administrator, long timestamp, String uuid, HttpServletRequest httpServletRequest) throws Exception{ String clientUrl = Utils.getCurrentClientUrl(httpServletRequest).split("\\?")[0]; // ignore other parameters RevertOperationUrl operationUrl = new RevertOperationUrl(clientUrl, administrator, timestamp, uuid, operation); String shortUrl = operationUrl.getShortUrl(); diff --git a/src/main/java/org/gcube/datacatalogue/grsf_manage_widget/server/manage/Utils.java b/src/main/java/org/gcube/datacatalogue/grsf_manage_widget/server/manage/Utils.java index 2f4c523..1815514 100644 --- a/src/main/java/org/gcube/datacatalogue/grsf_manage_widget/server/manage/Utils.java +++ b/src/main/java/org/gcube/datacatalogue/grsf_manage_widget/server/manage/Utils.java @@ -28,6 +28,7 @@ import org.gcube.datacatalogue.common.enums.Product_Type; import org.gcube.datacatalogue.common.enums.Status; import org.gcube.datacatalogue.grsf_manage_widget.shared.ConnectedBean; import org.gcube.datacatalogue.grsf_manage_widget.shared.ManageProductBean; +import org.gcube.datacatalogue.grsf_manage_widget.shared.RevertableOperationInfo; import org.gcube.datacatalogue.grsf_manage_widget.shared.SimilarGRSFRecord; import org.gcube.resources.discovery.client.api.DiscoveryClient; import org.gcube.resources.discovery.client.queries.api.Query; @@ -212,7 +213,7 @@ public class Utils { try{ // send email to Editors and Reviewers - SocialCommunications.sendEmailAdministrators(bean, catalogue, username, fullName, groupId, httpServletRequest, bean.isMergesInvolved()); + SocialCommunications.sendEmailAdministratorsOnMerge(bean, catalogue, username, fullName, groupId, httpServletRequest, bean.isMergesInvolved()); // create a post about the operation SocialCommunications.writeProductPost(bean, username, fullName, report, true); @@ -232,6 +233,40 @@ public class Utils { } return null; } + + /** + * Revert operation and alert admins/vre users + * @author Costantino Perciante at ISTI-CNR (costantino.perciante@isti.cnr.it) + * @param httpClient + * @param baseUrl + * @param fullName + * @param uuid + */ + public static void revertOperation(CloseableHttpClient httpClient, String baseUrl, + RevertableOperationInfo rInfo, String token, String context, long groupId) throws Exception{ + + GRSFUpdaterServiceClient.revertOperation(httpClient, baseUrl, rInfo.getFullName(), rInfo.getUuid()); + + // manage interactions through a separated thread but set there security token and context (and then reset them) + new Thread(()->{ + + ScopeProvider.instance.set(context); + SecurityTokenProvider.instance.set(token); + try{ + + // create a post about the operation + SocialCommunications.sendEmailAdministratorsOnOperationReverted(rInfo, groupId); + + }catch(Exception e){ + logger.error("Something failed while alerting editors/reviewers", e); + }finally{ + ScopeProvider.instance.reset(); + SecurityTokenProvider.instance.reset(); + } + + }).start(); + + } /** * Update the status of the involved records to "to be merged" @@ -396,33 +431,6 @@ public class Utils { return null; } - // /** - // * Get extra information to show in the management panel, if any - // * @param extrasAsPairs - // */ - // public static void getExtrasToShow(List extrasAsPairs, ){ - // - // Set extrasToShow = getLookedUpExtrasKeys(); - // if(extrasToShow != null && !extrasToShow.isEmpty()){ - // Map extrasKeyValuePair = new HashMap(); - // = product.getExtras(); - // for (CkanPair ckanPair : extrasAsPairs) { - // String key = ckanPair.getKey(); - // String value = ckanPair.getValue(); - // - // if(extrasToShow.contains(key)){ - // String currentValueInMap = extrasKeyValuePair.get(key); - // if(currentValueInMap == null) - // currentValueInMap = value; - // else - // currentValueInMap += ", " + value; - // extrasKeyValuePair.put(key, currentValueInMap); - // } - // } - // toReturn.setExtrasIfAvailable(extrasKeyValuePair); - // } - // - // } /** * Get a {@link SimilarGRSFRecord} from a json string diff --git a/src/main/java/org/gcube/datacatalogue/grsf_manage_widget/shared/RevertableOperationInfo.java b/src/main/java/org/gcube/datacatalogue/grsf_manage_widget/shared/RevertableOperationInfo.java new file mode 100644 index 0000000..5e61c9e --- /dev/null +++ b/src/main/java/org/gcube/datacatalogue/grsf_manage_widget/shared/RevertableOperationInfo.java @@ -0,0 +1,79 @@ +package org.gcube.datacatalogue.grsf_manage_widget.shared; + +import java.io.Serializable; + +public class RevertableOperationInfo implements Serializable{ + + private static final long serialVersionUID = 5274434342849474800L; + private String recordUrl; + private String fullName; + private String uuid; + private String admin; + private long timestamp; + private RevertableOperations operation; + + public RevertableOperationInfo() { + super(); + } + + public RevertableOperationInfo(String recordUrl, String fullName, + String uuid, String admin, long timestamp, RevertableOperations operation) { + super(); + this.recordUrl = recordUrl; + this.fullName = fullName; + this.uuid = uuid; + this.admin = admin; + this.timestamp = timestamp; + this.operation = operation; + } + + public String getAdmin() { + return admin; + } + + public void setAdmin(String admin) { + this.admin = admin; + } + + public long getTimestamp() { + return timestamp; + } + + public void setTimestamp(long timestamp) { + this.timestamp = timestamp; + } + + public RevertableOperations getOperation() { + return operation; + } + + public void setOperation(RevertableOperations operation) { + this.operation = operation; + } + + public String getRecordUrl() { + return recordUrl; + } + public void setRecordUrl(String recordUrl) { + this.recordUrl = recordUrl; + } + public String getFullName() { + return fullName; + } + public void setFullName(String fullName) { + this.fullName = fullName; + } + public String getUuid() { + return uuid; + } + public void setUuid(String uuid) { + this.uuid = uuid; + } + + @Override + public String toString() { + return "RevertableOperationInfo [recordUrl=" + recordUrl + ", fullName=" + + fullName + ", uuid=" + uuid + ", admin=" + admin + + ", timestamp=" + timestamp + ", operation=" + operation + "]"; + } +} diff --git a/src/main/java/org/gcube/datacatalogue/grsf_manage_widget/shared/RevertableOperations.java b/src/main/java/org/gcube/datacatalogue/grsf_manage_widget/shared/RevertableOperations.java new file mode 100644 index 0000000..ca0b7a8 --- /dev/null +++ b/src/main/java/org/gcube/datacatalogue/grsf_manage_widget/shared/RevertableOperations.java @@ -0,0 +1,22 @@ +package org.gcube.datacatalogue.grsf_manage_widget.shared; + +/** + * For now only Merge can be reverted + * @author Costantino Perciante at ISTI-CNR (costantino.perciante@isti.cnr.it) + */ +public enum RevertableOperations { + + MERGE("merge"); + // DISSECT("dissect"); + private String name; + + private RevertableOperations(String name) { + this.name = name; + } + + @Override + public String toString() { + return name; + } + +} \ No newline at end of file