package org.gcube.datacatalogue.grsf_manage_widget.server.manage; import static org.gcube.resources.discovery.icclient.ICFactory.clientFor; import static org.gcube.resources.discovery.icclient.ICFactory.queryFor; import java.text.SimpleDateFormat; import java.util.Date; import java.util.Iterator; import java.util.List; import java.util.Map; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpSession; import org.gcube.common.portal.PortalContext; import org.gcube.common.resources.gcore.ServiceEndpoint; import org.gcube.common.resources.gcore.ServiceEndpoint.AccessPoint; import org.gcube.common.scope.api.ScopeProvider; import org.gcube.datacatalogue.ckanutillibrary.server.ApplicationProfileScopePerUrlReader; 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.grsf_manage_widget.client.GRSFManageWidgetService; import org.gcube.datacatalogue.grsf_manage_widget.shared.GRSFStatus; import org.gcube.datacatalogue.grsf_manage_widget.shared.ManageProductBean; import org.gcube.datacatalogue.grsf_manage_widget.shared.ex.NoGRSFRecordException; import org.gcube.resources.discovery.client.api.DiscoveryClient; import org.gcube.resources.discovery.client.queries.api.SimpleQuery; import org.gcube.vomanagement.usermanagement.impl.LiferayRoleManager; import org.gcube.vomanagement.usermanagement.impl.LiferayUserManager; import org.gcube.vomanagement.usermanagement.model.GCubeRole; import org.gcube.vomanagement.usermanagement.model.GCubeUser; import org.json.simple.JSONArray; import org.json.simple.JSONObject; import org.json.simple.parser.JSONParser; import com.google.gwt.user.server.rpc.RemoteServiceServlet; import com.liferay.portal.kernel.log.Log; import com.liferay.portal.kernel.log.LogFactoryUtil; import eu.trentorise.opendata.jackan.internal.org.apache.http.HttpResponse; import eu.trentorise.opendata.jackan.internal.org.apache.http.client.methods.HttpPost; import eu.trentorise.opendata.jackan.internal.org.apache.http.entity.StringEntity; import eu.trentorise.opendata.jackan.internal.org.apache.http.impl.client.CloseableHttpClient; import eu.trentorise.opendata.jackan.internal.org.apache.http.impl.client.HttpClientBuilder; import eu.trentorise.opendata.jackan.internal.org.apache.http.util.EntityUtils; import eu.trentorise.opendata.jackan.model.CkanDataset; import eu.trentorise.opendata.jackan.model.CkanGroup; import eu.trentorise.opendata.jackan.model.CkanPair; import eu.trentorise.opendata.jackan.model.CkanTag; /** * Endpoint for sending update records information to GRSF KB * @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 String SERVICE_POST_METHOD = "/service/updater/post"; private static final String ANNOTATION_KEY = "Annotation on update"; private static final SimpleDateFormat DATE_FORMAT = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"); private static final String STATUS_CUSTOM_FIELD_KEY = "Status"; private static final int MAX_TRIAL = 5; public static final String GRSF_UPDATER_SERVICE = "GRSFUpdaterEndPoint"; private static final String GRSF_ADMIN_ROLE = "GRSF-Admin"; // request post fields private static final String CATALOGUE_ID = "catalog_id"; private static final String KB_ID = "record_id"; private static final String PRODUCT_TYPE = "type"; private static final String STATUS = "status"; private static final String ANNOTATION = "annotation_msg"; private static final String ERROR = "error"; // the error of the update on success private static final int STATUS_SUCCESS = 200; // GRSF update service information private static final String SERVICE_NAME = "GRSF Updater"; private static final String SERVICE_CATEGORY = "Service"; // request url public static final String GCUBE_REQUEST_URL = "gcube-request-url"; // session info for user public static final String GRSF_ADMIN_SESSION_KEY = "IS_GRSF_ADMIN"; /** * 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 */ public DataCatalogue getCatalogue(String discoverScope){ String currentScope = getCurrentContext(getThreadLocalRequest(), false); 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()); } return instance; } @Override public String notifyProductUpdate(ManageProductBean bean) { logger.info("Creating notification for the bean " + bean + " to send to the knowledge base"); try{ String context = getScopeFromClientUrl(getThreadLocalRequest()); DataCatalogue catalogue = getCatalogue(context); // check if the base url of the service is in session String keyPerContext = UtilMethods.concatenateSessionKeyScope(GRSF_UPDATER_SERVICE, context); String baseUrl = (String)getThreadLocalRequest().getSession().getAttribute(keyPerContext); if(baseUrl == null || baseUrl.isEmpty()){ baseUrl = GRSFNotificationService.discoverEndPoint(context); getThreadLocalRequest().getSession().setAttribute(keyPerContext, baseUrl); } return GRSFNotificationService.updateCatalogueRecord(baseUrl, bean, catalogue, getCurrentUser(getThreadLocalRequest()).getUsername()); }catch(Exception e){ logger.error("Unable to update the product.." + e.getMessage()); return e.getMessage(); } } @Override public ManageProductBean getProductBeanById(String productIdentifier) throws Exception { ManageProductBean toReturn = null; // retrieve scope per current portlet url String scopePerCurrentUrl = getScopeFromClientUrl(getThreadLocalRequest()); DataCatalogue catalogue = getCatalogue(scopePerCurrentUrl); String username = getCurrentUser(getThreadLocalRequest()).getUsername(); CkanDataset product = catalogue.getDataset(productIdentifier, catalogue.getApiKeyFromUsername(username)); // get extras Map extras = product.getExtrasAsHashMap(); String status = extras.get("Status"); String uuidKB = extras.get("UUID Knowledge Base"); String productType = extras.get("Product type"); String recordType = extras.get("Record type"); String title = product.getTitle(); // it cannot be enabled in this case ... if(recordType == null || recordType.equals("Source")) throw new NoGRSFRecordException("This is not a GRSF Item"); if(status == null || uuidKB == null || productType == null) throw new Exception("Some information is missing in this record: Status = " + status + ", knowledge_base_uuid = " + uuidKB + ", and product type is = " + productType); toReturn = new ManageProductBean(); toReturn.setCatalogueIdentifier(productIdentifier); toReturn.setCurrentStatus(GRSFStatus.fromString(status)); toReturn.setKnowledgeBaseIdentifier(uuidKB); toReturn.setProductName(title); toReturn.setProductType(productType); logger.info("Returning item bean " + toReturn); return toReturn; } /** * Discover the service endpoint and return its url * @param context * @return the url of the service on success, null otherwise */ public static String discoverEndPoint(String context){ String oldContext = ScopeProvider.instance.get(); ScopeProvider.instance.set(context); String toReturn = null; try{ SimpleQuery query = queryFor(ServiceEndpoint.class); query.addCondition("$resource/Profile/Name/text() eq '"+ SERVICE_NAME +"'"); query.addCondition("$resource/Profile/Category/text() eq '"+ SERVICE_CATEGORY +"'"); DiscoveryClient client = clientFor(ServiceEndpoint.class); List resources = client.submit(query); if (resources.size() == 0){ logger.error("There is no Runtime Resource having name " + SERVICE_NAME +" and Category " + SERVICE_CATEGORY + " in this scope."); throw new Exception("There is no Runtime Resource having name " + SERVICE_NAME +" and Category " + SERVICE_CATEGORY + " in this scope."); } else { for (ServiceEndpoint res : resources) { Iterator accessPointIterator = res.profile().accessPoints().iterator(); while (accessPointIterator.hasNext()) { ServiceEndpoint.AccessPoint accessPoint = (ServiceEndpoint.AccessPoint) accessPointIterator .next(); // return the path toReturn = accessPoint.address(); } } } }catch(Exception e){ logger.error("Unable to retrieve such service endpoint information!", e); }finally{ if(oldContext != null && !oldContext.equals(context)) ScopeProvider.instance.set(oldContext); } return toReturn; } /** * Send an update for this bean * @param baseUrl * @param bean * @param username * @param catalogue * @return true on success, false otherwise */ @SuppressWarnings("unchecked") public static String updateCatalogueRecord(String serviceUrl, ManageProductBean bean, DataCatalogue catalogue, String username){ if(serviceUrl == null) throw new IllegalArgumentException("GRSF Updater service url cannot be null"); if(bean == null) throw new IllegalArgumentException("Item bean to manage cannot be null"); try(CloseableHttpClient httpClient = HttpClientBuilder.create().build();){ JSONObject obj = new JSONObject(); obj.put(CATALOGUE_ID, bean.getCatalogueIdentifier()); obj.put(KB_ID, bean.getKnowledgeBaseIdentifier()); obj.put(PRODUCT_TYPE, bean.getProductType().toLowerCase()); obj.put(STATUS, bean.getNewStatus().toString().toLowerCase()); String annotation = bean.getAnnotation(); if(annotation != null) obj.put(ANNOTATION, annotation.replaceAll("\"", "")); logger.debug("Update request looks like " + obj.toJSONString()); HttpPost request = new HttpPost(serviceUrl + SERVICE_POST_METHOD); request.setHeader("Accept", "application/json"); request.setHeader("Content-type", "application/json"); StringEntity params = new StringEntity(obj.toJSONString()); request.setEntity(params); HttpResponse response = httpClient.execute(request); logger.debug("Response code is " + response.getStatusLine().getStatusCode() + " and response message is " + response.getStatusLine().getReasonPhrase()); String result = EntityUtils.toString(response.getEntity()); JSONParser parser = new JSONParser(); JSONObject parsedJSON = (JSONObject)parser.parse(result); if(response.getStatusLine().getStatusCode() != STATUS_SUCCESS) throw new IllegalArgumentException( "Error while performing the update request: " + response.getStatusLine().getReasonPhrase() + "and error in the result bean is " + parsedJSON.get(ERROR)); // patch the catalogue product return patchProduct(catalogue, bean, username); }catch(Exception e){ logger.error("Unable to update this Item " + e.getMessage()); return e.getMessage(); } } /** * Patch the product * @param catalogue * @param bean * @param username */ @SuppressWarnings("unchecked") private static String patchProduct(DataCatalogue catalogue, ManageProductBean bean, String username) { logger.info("Going to patch record in the catalogue with identifier " + bean.getCatalogueIdentifier() + " from user " + username); String apiKey = catalogue.getApiKeyFromUsername(username); CkanDataset dataset = catalogue.getDataset(bean.getCatalogueIdentifier(), apiKey); String errorMessage = null; for (int i = 0; i < MAX_TRIAL; i++) { try(CloseableHttpClient httpClient = HttpClientBuilder.create().build();){ JSONObject jsonRequest = new JSONObject(); JSONArray tagsAsJson = new JSONArray(); JSONArray groupsAsJson = new JSONArray(); JSONArray customFieldsAsJson = new JSONArray(); // manage the custom fields List extras = dataset.getExtras(); for (CkanPair ckanPair : extras) { if(ckanPair.getKey().equals(STATUS_CUSTOM_FIELD_KEY) && ckanPair.getValue().equals(bean.getCurrentStatus().toString())) continue; JSONObject obj = new JSONObject(); obj.put("key", ckanPair.getKey()); obj.put("value", ckanPair.getValue()); customFieldsAsJson.add(obj); } // add the new one and the annotation message JSONObject newStatus = new JSONObject(); newStatus.put("key", STATUS_CUSTOM_FIELD_KEY); newStatus.put("value", bean.getNewStatus().toString()); customFieldsAsJson.add(newStatus); JSONObject newAnnotation = new JSONObject(); newAnnotation.put("key", ANNOTATION_KEY); newAnnotation.put("value", "date: " + DATE_FORMAT.format(new Date()) + ", admin: " + new LiferayUserManager().getUserByUsername(username).getFullname() + ", message: " + (bean.getAnnotation() != null ? bean.getAnnotation().replaceAll("\"", "") : "none") + ", old status: " + bean.getCurrentStatus().toString() + ", new status: " + bean.getNewStatus().toString() ); customFieldsAsJson.add(newAnnotation); // manage the tags List tags = dataset.getTags(); for(CkanTag ckanTag : tags){ if(!ckanTag.getName().equals(bean.getCurrentStatus().toString())){ JSONObject obj = new JSONObject(); obj.put("vocabulary_id", ckanTag.getVocabularyId()); obj.put("state", ckanTag.getState().toString()); obj.put("display_name", ckanTag.getDisplayName()); obj.put("id", ckanTag.getId()); obj.put("name", ckanTag.getName()); tagsAsJson.add(obj); } } // add the new one JSONObject newTag = new JSONObject(); newTag.put("name", bean.getNewStatus().toString()); newTag.put("display_name", bean.getNewStatus().toString()); tagsAsJson.add(newTag); // manage the groups List groups = dataset.getGroups(); for (CkanGroup ckanGroup : groups) { if(!ckanGroup.getName().equals("grsf" + "-" + bean.getCurrentStatus().toString().toLowerCase())){ JSONObject obj = new JSONObject(); obj.put("name", ckanGroup.getName()); groupsAsJson.add(obj); } } JSONObject newGroup = new JSONObject(); newGroup.put("name", "grsf" + "-" + bean.getNewStatus().toString().toLowerCase()); groupsAsJson.add(newGroup); // perform the request jsonRequest.put("id", bean.getCatalogueIdentifier()); jsonRequest.put("tags", tagsAsJson); jsonRequest.put("extras", customFieldsAsJson); jsonRequest.put("groups", groupsAsJson); logger.debug("Request param is going to be " + jsonRequest); if((errorMessage = catalogue.patchProductWithJSON(bean.getCatalogueIdentifier(), jsonRequest, apiKey)) == null){ logger.info("Record patched ..."); break; }else continue; // retry }catch(Exception e){ logger.error("Error while trying to patch grsf record (iteration " + i + " of " + MAX_TRIAL + ")" + e.getMessage()); errorMessage = e.getMessage(); } } return errorMessage; } /** * Get the scope in which ckan information needs to be discovered from the url * @param httpServletRequest * @return */ public static String getScopeFromClientUrl(HttpServletRequest httpServletRequest){ if(httpServletRequest == null) throw new IllegalArgumentException("HttpServletRequest is null!"); String scopeToReturn = null; try{ String clientUrl = getCurrentClientUrl(httpServletRequest).split("\\?")[0]; logger.debug("Client url is " + clientUrl); // check if this information is in session, otherwise set it and return HttpSession session = httpServletRequest.getSession(); if((scopeToReturn = (String) session.getAttribute(clientUrl)) != null){ logger.debug("Scope to return is " + scopeToReturn); }else{ // ask to the ckan library and set it scopeToReturn = ApplicationProfileScopePerUrlReader.getScopePerUrl(clientUrl); logger.debug("Scope to return is " + scopeToReturn); session.setAttribute(clientUrl, scopeToReturn); } }catch(Exception e){ scopeToReturn = getCurrentContext(httpServletRequest, false); logger.warn("Failed to determine the scope from the client url, returning the current one: " + scopeToReturn); } return scopeToReturn; } /** * Needed to get the url of the client * @param httpServletRequest the httpServletRequest object * @return the instance of the user * @see the url at client side */ public static String getCurrentClientUrl(HttpServletRequest httpServletRequest) { if(httpServletRequest == null) throw new IllegalArgumentException("HttpServletRequest is null!"); return httpServletRequest.getHeader(GCUBE_REQUEST_URL); } /** * Retrieve the current scope by using the portal manager * @param b * @return a GcubeUser object */ public static String getCurrentContext(HttpServletRequest request, boolean setInThread){ if(request == null) throw new IllegalArgumentException("HttpServletRequest is null!"); PortalContext pContext = PortalContext.getConfiguration(); String context = pContext.getCurrentScope(request); logger.debug("Returning context " + context); if(context != null && setInThread) ScopeProvider.instance.set(context); return context; } /** * Retrieve the current user by using the portal manager * @return a GcubeUser object */ public static GCubeUser getCurrentUser(HttpServletRequest request){ if(request == null) throw new IllegalArgumentException("HttpServletRequest is null!"); PortalContext pContext = PortalContext.getConfiguration(); GCubeUser user = pContext.getCurrentUser(request); logger.debug("Returning user " + user); return user; } @Override public boolean isAdminUser() { try{ Boolean inSession = (Boolean)getThreadLocalRequest().getSession().getAttribute(GRSF_ADMIN_SESSION_KEY); if(inSession != null) return inSession; else{ PortalContext pContext = PortalContext.getConfiguration(); List userRoles = new LiferayRoleManager().listRolesByUserAndGroup( pContext.getCurrentUser(getThreadLocalRequest()).getUserId(), pContext.getCurrentGroupId(getThreadLocalRequest())); boolean toSetInSession = false; for (GCubeRole gCubeRole : userRoles) { if(gCubeRole.getRoleName().equals(GRSF_ADMIN_ROLE)){ toSetInSession = true; break; } } getThreadLocalRequest().getSession().setAttribute(GRSF_ADMIN_SESSION_KEY, toSetInSession); return toSetInSession; } }catch(Exception e){ logger.error("Failed to check if the user has role " + GRSF_ADMIN_ROLE, e); } return false; } }