package org.gcube.datacatalogue.grsf_manage_widget.server.manage; import java.util.ArrayList; import java.util.List; import java.util.Map; import java.util.UUID; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpSession; import org.gcube.common.authorization.library.provider.SecurityTokenProvider; import org.gcube.common.portal.PortalContext; import org.gcube.common.scope.api.ScopeProvider; import org.gcube.datacatalogue.ckanutillibrary.server.DataCatalogue; import org.gcube.datacatalogue.ckanutillibrary.server.DataCatalogueFactory; import org.gcube.datacatalogue.ckanutillibrary.server.utils.UtilMethods; import org.gcube.datacatalogue.common.Constants; import org.gcube.datacatalogue.common.enums.Product_Type; import org.gcube.datacatalogue.common.enums.Sources; 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.SimilarGRSFRecord; import org.gcube.datacatalogue.grsf_manage_widget.shared.SourceRecord; import org.gcube.datacatalogue.grsf_manage_widget.shared.ex.NoGRSFRecordException; import org.gcube.vomanagement.usermanagement.RoleManager; import org.gcube.vomanagement.usermanagement.impl.LiferayRoleManager; import org.gcube.vomanagement.usermanagement.model.GCubeTeam; import org.gcube.vomanagement.usermanagement.model.GCubeUser; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import com.google.gwt.user.server.rpc.RemoteServiceServlet; 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.model.CkanDataset; import eu.trentorise.opendata.jackan.model.CkanPair; import eu.trentorise.opendata.jackan.model.CkanResource; /** * Endpoint for sending update records information to GRSF KnowledgeBase. * @author Costantino Perciante at ISTI-CNR (costantino.perciante@isti.cnr.it) */ public class GRSFNotificationService extends RemoteServiceServlet implements GRSFManageWidgetService{ private static final long serialVersionUID = -4534905087994875893L; //private static final Log logger = LogFactoryUtil.getLog(GRSFNotificationService.class); private static final Logger logger = LoggerFactory.getLogger(GRSFNotificationService.class); /** * Instanciate the ckan util library. * Since it needs the scope, we need to check if it is null or not * @param discoverScope if you want to the discover the utils library in this specified scope * @return DataCatalogue object * @throws Exception */ public DataCatalogue getCatalogue(String discoverScope) throws Exception{ String currentScope = Utils.getCurrentContext(getThreadLocalRequest(), true); DataCatalogue instance = null; try{ String scopeInWhichDiscover = (discoverScope != null && !discoverScope.isEmpty()) ? discoverScope : currentScope; logger.debug("Discovering ckan utils library into scope " + scopeInWhichDiscover); instance = DataCatalogueFactory.getFactory().getUtilsPerScope(scopeInWhichDiscover); }catch(Exception e){ logger.error("Unable to retrieve ckan utils. Error was " + e.toString()); throw e; } return instance; } @Override public ManageProductBean getProductBeanById(String productIdentifier) throws Exception { ManageProductBean toReturn = null; // check into session first HttpSession httpSession = getThreadLocalRequest().getSession(); String sessionProductKey = ScopeProvider.instance.get() + productIdentifier; if(httpSession.getAttribute(sessionProductKey) != null) return (ManageProductBean) httpSession.getAttribute(sessionProductKey); if(!Utils.isIntoPortal()){ toReturn = new ManageProductBean(); toReturn.setCatalogueIdentifier(UUID.randomUUID().toString()); List connectTo = new ArrayList<>(); connectTo.add(new ConnectedBean( "91f1e413-dc9f-3b4e-b1c5-0e8560177253", "Stock", "http://data.d4science.org/ctlg/GRSF_Admin/91f1e413-dc9f-3b4e-b1c5-0e8560177253", "89f1e413-dc9f-3b4e-b1c5-0e8560177254", "Random title", "http://data.d4science.org/ctlg/GRSF_Admin/89f1e413-dc9f-3b4e-b1c5-0e8560177254", "Fishery" )); toReturn.setCurrentConnections(connectTo); toReturn.setGrsfDomain("Stock"); toReturn.setGrsfType("Assessment Unit"); toReturn.setKnowledgeBaseIdentifier("91f1e413-dc9f-3b4e-b1c5-0e8560177253"); toReturn.setShortName("Widow rockfish - US West Coast"); toReturn.setShortNameUpdated("Widow rockfish - US West Coast"); toReturn.setGrsfName("sebastes entomelas FAO 77 FAO 67"); toReturn.setTraceabilityFlag(true); toReturn.setCurrentStatus(Status.Pending); toReturn.setSemanticIdentifier("asfis:WRO+fao:67;FAO"); ArrayList sources = new ArrayList(); sources.add(new SourceRecord("RAM", "http://www.google.it")); sources.add(new SourceRecord("FIRMS", "http://www.google.it")); sources.add(new SourceRecord("FishSource", "http://www.google.it")); toReturn.setSources(sources); List similarGrsfRecords = new ArrayList(); similarGrsfRecords.add(new SimilarGRSFRecord("same species overlapping water areas", Utils.getDatasetKnowledgeBaseIdFromUrl("http://data.d4science.org/ctlg/GRSF_Admin/1a03d6d8-002d-39b9-9255-a954c8ee2bf0") ,"unknown:ONCORHYNCHUS GORBUSCHA+unknown:USA-AKSTATE-KELPB", "Pink Salmon Kelp By (District112)", "http://data.d4science.org/ctlg/GRSF_Admin/1a03d6d8-002d-39b9-9255-a954c8ee2bf0")); similarGrsfRecords.add(new SimilarGRSFRecord("same species overlapping water areas 2", Utils.getDatasetKnowledgeBaseIdFromUrl("http://data.d4science.org/ctlg/GRSF_Admin/1a03d6d8-002d-39b9-9255-a954c8ee2bf0"), "unknown:ONCORHYNCHUS GORBUSCHA+unknown:USA-AKSTATE-KELPB", "Pink Salmon Kelp By (District112) 2", "http://data.d4science.org/ctlg/GRSF_Admin/1a03d6d8-002d-39b9-9255-a954c8ee2bf0")); toReturn.setSimilarGrsfRecords(similarGrsfRecords); }else{ String scopePerCurrentUrl = Utils.getScopeFromClientUrl(getThreadLocalRequest()); DataCatalogue catalogue = getCatalogue(scopePerCurrentUrl); String username = Utils.getCurrentUser(getThreadLocalRequest()).getUsername(); String apiKey = catalogue.getApiKeyFromUsername(username); CkanDataset record = catalogue.getDataset(productIdentifier, apiKey); // it cannot be enabled in this case ... if(record == null) throw new Exception("Unable to retrieve information for the selected record, sorry"); else{ logger.debug("Trying to fetch the record...."); // check it is a grsf record (Source records have a different System Type) Map extrasAsMap = record.getExtrasAsHashMap(); // get extras as hashmap and pairs List extrasAsPairs = record.getExtras(); String systemType = extrasAsMap.get(Constants.SYSTEM_TYPE_CUSTOM_KEY); if(systemType == null || systemType.isEmpty() || systemType.equals(Constants.SYSTEM_TYPE_FOR_SOURCES_VALUE)) throw new NoGRSFRecordException("This is not a GRSF Record"); boolean isStock = record.getExtrasAsHashMap().get(Constants.DOMAIN_CUSTOM_KEY).contains(Product_Type.STOCK.getOrigName()); // fetch map for namespaces Map fieldsNamespacesMap = Utils.getFieldToFieldNameSpaceMapping(httpSession, isStock ? Constants.GENERIC_RESOURCE_NAME_MAP_KEY_NAMESPACES_STOCK : Constants.GENERIC_RESOURCE_NAME_MAP_KEY_NAMESPACES_FISHERY); Map> extrasWithoutNamespaces = Utils.replaceFieldsKey(extrasAsPairs, fieldsNamespacesMap); // get extras fields (wrt the mandatory ones) to show in the management panel TODO // Utils.getExtrasToShow(); String catalogueIdentifier = record.getId(); Status status = Status.fromString(extrasWithoutNamespaces.get(Constants.STATUS_OF_THE_GRSF_RECORD_CUSTOM_KEY).get(0)); String uuidKB = extrasWithoutNamespaces.get(Constants.UUID_KB_CUSTOM_KEY).get(0); String grsfDomain = extrasWithoutNamespaces.get(Constants.DOMAIN_CUSTOM_KEY).get(0); if(status == null || uuidKB == null) throw new Exception("Some information is missing in this record: Status = " + status + ", knowledge base uuid = " + uuidKB + ", and grsf domain is = " + grsfDomain); if(status.equals(Status.To_be_Merged) || status.equals(Status.Rejected)) throw new Exception("Records under merging activity or rejected cannot be updated"); String semanticId = extrasWithoutNamespaces.get(Constants.GRSF_SEMANTIC_IDENTIFIER_CUSTOM_KEY).get(0); String shortName = extrasWithoutNamespaces.get(Constants.SHORT_NAME_CUSTOM_KEY).get(0); String grsfType = extrasWithoutNamespaces.get(Constants.GRSF_TYPE_CUSTOM_KEY).get(0); String recordUrl = extrasWithoutNamespaces.get(Constants.ITEM_URL_FIELD).get(0); String grsfName = extrasWithoutNamespaces.get(grsfDomain.contains(Product_Type.STOCK.getOrigName()) ? Constants.STOCK_NAME_CUSTOM_KEY : Constants.FISHERY_NAME_CUSTOM_KEY).get(0); boolean traceabilityFlag = false; try{ traceabilityFlag = extrasWithoutNamespaces.get(Constants.TRACEABILITY_FLAG_CUSTOM_KEY).get(0).equalsIgnoreCase("true"); }catch(Exception e){ logger.warn("Unable to fetch traceability flag", e); } // Get similar GRSF records, if any (each of which should have name, description, url and id(i.e semantic identifier)) List similarGrsfRecordsAsStrings = extrasWithoutNamespaces.containsKey(Constants.SIMILAR_GRSF_RECORDS_CUSTOM_KEY) ? extrasWithoutNamespaces.get(Constants.SIMILAR_GRSF_RECORDS_CUSTOM_KEY): null; List similarRecords = new ArrayList(0); if(similarGrsfRecordsAsStrings != null){ for (String similarGRSFRecord : similarGrsfRecordsAsStrings) { if(similarGRSFRecord.equals(Constants.NO_SIMILAR_GRSF_RECORDS)) // stop here if there is a single element with this information break; similarRecords.add(Utils.similarGRSFRecordFromJson(similarGRSFRecord)); } } logger.debug("SimilarGRSFRecords are " + similarRecords); // get connected records (and the proposed ones) List connectedBeanUrls = extrasWithoutNamespaces.containsKey(Constants.CONNECTED_CUSTOM_KEY) ? extrasWithoutNamespaces.get(Constants.CONNECTED_CUSTOM_KEY): null; List connectedBeans = new ArrayList(0); if(connectedBeanUrls != null){ for (String connectedBean : connectedBeanUrls) { if(connectedBean.equals(Constants.NO_CONNECTED_RECORDS)) // stop here if there is a single element with this information break; ConnectedBean builtBean = Utils.connectedBeanRecordFromUrl(recordUrl, connectedBean, uuidKB, grsfDomain, catalogue, apiKey); if(builtBean != null) connectedBeans.add(builtBean); } } logger.debug("Already connected records are " + connectedBeans); // get the connections the knowledge base suggests List suggestedConnectionsByKnowledgeBase = new ArrayList(0); List exploitedResourcesUrls = isStock ? (extrasWithoutNamespaces.containsKey(Constants.EXPLOITING_FISHERY_JSON_KEY) ? extrasWithoutNamespaces.get(Constants.EXPLOITING_FISHERY_JSON_KEY) : null): (extrasWithoutNamespaces.containsKey(Constants.RESOURCES_EXPLOITED_JSON_KEY) ? extrasWithoutNamespaces.get(Constants.RESOURCES_EXPLOITED_JSON_KEY) : null); if(exploitedResourcesUrls != null && !exploitedResourcesUrls.isEmpty()){ for (String exploited : exploitedResourcesUrls) { ConnectedBean builtBean = Utils.connectedBeanRecordFromUrl(recordUrl, exploited, uuidKB, grsfDomain, catalogue, apiKey); if(builtBean != null) suggestedConnectionsByKnowledgeBase.add(builtBean); } } logger.debug("Knowledge base suggests " + suggestedConnectionsByKnowledgeBase); // Get sources List resources = record.getResources(); List sources = new ArrayList(3); for (CkanResource ckanResource : resources) { if(Sources.getListNames().contains(ckanResource.getName())) sources.add(new SourceRecord(ckanResource.getName(), ckanResource.getUrl())); } // set the values toReturn = new ManageProductBean(semanticId, catalogueIdentifier, uuidKB, grsfType, grsfDomain, grsfName, shortName, traceabilityFlag, status, recordUrl, null, sources, similarRecords, connectedBeans, suggestedConnectionsByKnowledgeBase); } } logger.info("Returning item bean " + toReturn); httpSession.setAttribute(sessionProductKey, toReturn); return toReturn; } @Override public boolean isAdminUser() { try{ Boolean inSession = (Boolean)getThreadLocalRequest().getSession().getAttribute(Constants.GRSF_ADMIN_SESSION_KEY); if(inSession != null) return inSession; else{ boolean toSetInSession = false; if(!Utils.isIntoPortal()){ toSetInSession = true; }else{ PortalContext pContext = PortalContext.getConfiguration(); RoleManager roleManager = new LiferayRoleManager(); String username = pContext.getCurrentUser(getThreadLocalRequest()).getUsername(); long userId = pContext.getCurrentUser(getThreadLocalRequest()).getUserId(); long groupId = pContext.getCurrentGroupId(getThreadLocalRequest()); List teamRoles = roleManager.listTeamsByUserAndGroup(userId, groupId); toSetInSession = isEditor(username, teamRoles) | isReviewer(username, teamRoles); } getThreadLocalRequest().getSession().setAttribute(Constants.GRSF_ADMIN_SESSION_KEY, toSetInSession); return toSetInSession; } }catch(Exception e){ logger.error("Failed to check if the user belongs to team " + Constants.GRSF_CATALOGUE_EDITOR_ROLE + " or " + Constants.GRSF_CATALOGUE_REVIEWER_ROLE +"!", e); } return false; } @Override public String notifyProductUpdate(ManageProductBean bean) throws Exception{ logger.info("Creating notification for the bean " + bean + " to send to the knowledge base"); try{ String context = Utils.getScopeFromClientUrl(getThreadLocalRequest()); String token = SecurityTokenProvider.instance.get(); DataCatalogue catalogue = getCatalogue(context); String administratorFullName = Utils.getCurrentUser(getThreadLocalRequest()).getFullname(); String username = Utils.getCurrentUser(getThreadLocalRequest()).getUsername(); // check if the base url of the service is in session String keyPerContext = UtilMethods.concatenateSessionKeyScope(Constants.GRSF_UPDATER_SERVICE, context); HttpServletRequest threadRequest = getThreadLocalRequest(); String baseUrl = (String)threadRequest.getSession().getAttribute(keyPerContext); if(baseUrl == null || baseUrl.isEmpty()){ baseUrl = GRSFUpdaterServiceClient.discoverEndPoint(context); threadRequest.getSession().setAttribute(keyPerContext, baseUrl); } // remove it from the session String sessionProductKey = ScopeProvider.instance.get() + bean.getCatalogueIdentifier(); threadRequest.getSession().removeAttribute(sessionProductKey); return Utils.updateRecord(baseUrl, bean, catalogue, username, administratorFullName, threadRequest, PortalContext.getConfiguration().getCurrentGroupId(threadRequest), context, token); }catch(Exception e){ logger.error("Unable to update the product", e); throw e; } } @Override public void validateRevertOperation(String encryptedUrl) throws Exception { PortalContext pContext = PortalContext.getConfiguration(); String context = Utils.getScopeFromClientUrl(getThreadLocalRequest()); RoleManager roleManager = new LiferayRoleManager(); GCubeUser user = pContext.getCurrentUser(getThreadLocalRequest()); String username = user.getUsername(); String fullName = user.getFullname(); long userId = pContext.getCurrentUser(getThreadLocalRequest()).getUserId(); long groupId = pContext.getCurrentGroupId(getThreadLocalRequest()); List teamRoles = roleManager.listTeamsByUserAndGroup(userId, groupId); boolean isEditor = isEditor(username, teamRoles); boolean isReviewer = isReviewer(username, teamRoles); if(!(isEditor | isReviewer)) throw new Exception("You are not allowed to perform this operation!"); // decrypt the url RevertOperationUrl decryptedUrl = new RevertOperationUrl(encryptedUrl); String adminInUrl = decryptedUrl.getAdmin(); String uuid = decryptedUrl.getUuid(); logger.info("User " + username + " has requested to invert an operation on record with id " + uuid + " and admin in url was " + adminInUrl); // we need to check the timestamp (it has 24h validity) boolean isValidTimestamp = decryptedUrl.isTimestampValid(); 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!"); // 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(!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; } } /** * Check if the current user is an editor * @param username * @param teamRoles * @return true if he/she is an editor, false otherwise */ private boolean isEditor(String username, List teamRoles){ for (GCubeTeam team : teamRoles) { if(team.getTeamName().equals(Constants.GRSF_CATALOGUE_EDITOR_ROLE)){ logger.info("User " + username + " is allowed to modify GRSF records as editor"); return true; } } return false; } /** * Check if the current user is a reviewer * @param username * @param teamRoles * @return true if he/she is an reviewer, false otherwise */ private boolean isReviewer(String username, List teamRoles){ for (GCubeTeam team : teamRoles) { if(team.getTeamName().equals(team.getTeamName().equals(Constants.GRSF_CATALOGUE_REVIEWER_ROLE))){ logger.info("User " + username + " is allowed to modify GRSF records as reviewer"); return true; } } return false; } @Override public String checkIdentifierExists(String id) throws Exception { String scopePerCurrentUrl = Utils.getScopeFromClientUrl(getThreadLocalRequest()); DataCatalogue catalogue = getCatalogue(scopePerCurrentUrl); String username = Utils.getCurrentUser(getThreadLocalRequest()).getUsername(); CkanDataset dataset = catalogue.getDataset(id, catalogue.getApiKeyFromUsername(username)); if(dataset == null) throw new Exception("A record with id " + id + " doesn't exist"); return dataset.getExtrasAsHashMap().get(Constants.ITEM_URL_FIELD); } @Override public String checkIdentifierExistsInDomain(String id, String domain) throws Exception { String scopePerCurrentUrl = Utils.getScopeFromClientUrl(getThreadLocalRequest()); DataCatalogue catalogue = getCatalogue(scopePerCurrentUrl); String username = Utils.getCurrentUser(getThreadLocalRequest()).getUsername(); CkanDataset dataset = catalogue.getDataset(id, catalogue.getApiKeyFromUsername(username)); if(dataset == null) throw new Exception("A record with id " + id + " doesn't exist"); List extrasAsPairs = dataset.getExtras(); for (CkanPair ckanPair : extrasAsPairs) { if(ckanPair.getKey().contains(Constants.DOMAIN_CUSTOM_KEY)){ if(ckanPair.getValue().equalsIgnoreCase(domain)) return dataset.getExtrasAsHashMap().get(Constants.ITEM_URL_FIELD); } } throw new Exception("A record with id " + id + " doesn't exist in domain " + domain); } // @Override // public boolean checkSemanticIdentifierExists(String semanticIdentifier) // throws Exception { // // return getDataset(semanticIdentifier) != null; // } // // @Override // public boolean checkSemanticIdentifierExistsInDomain(String semanticIdentifier, String domain) // throws Exception { // CkanDataset dataset = getDataset(semanticIdentifier); // // // look for the right domain this time // List extrasAsPairs = dataset.getExtras(); // // for (CkanPair ckanPair : extrasAsPairs) { // if(ckanPair.getKey().contains(Constants.DOMAIN_CUSTOM_KEY)){ // return ckanPair.getValue().equalsIgnoreCase(domain); // } // } // // return false; // } // private CkanDataset getDataset(String semanticIdentifier) throws Exception{ // String scopePerCurrentUrl = Utils.getScopeFromClientUrl(getThreadLocalRequest()); // DataCatalogue catalogue = getCatalogue(scopePerCurrentUrl); // String username = Utils.getCurrentUser(getThreadLocalRequest()).getUsername(); // CkanDataset dataset = Utils.getRecordBySemanticIdentifier(semanticIdentifier, catalogue, catalogue.getApiKeyFromUsername(username)); // return dataset; // } }