Skip to content

Commit

Permalink
Added code for deleting individuals (#213)
Browse files Browse the repository at this point in the history
* Added code for deleting individuals

* Allow admins delete individuals

* Fixed delete individual url params

* Improved query safety

* refact: fixed constants' names, fixed mistakes, removed useless code

* refact: refactored sparql queries

* fix: fixed error logging mistakes

* fix: use CONSTRUCT instead of DESCRIBE as less potentionally problematic, lookup for mostSpecificTypes

* feat: add delete link in vitro individual profile if individual is editable

* fix: renamed individualURI to individualUri and added safety check

* fix: use ABox model to construct triples to delete
  • Loading branch information
litvinovg committed May 20, 2022
1 parent c418534 commit ff285fb
Show file tree
Hide file tree
Showing 11 changed files with 327 additions and 9 deletions.
@@ -0,0 +1,211 @@
package edu.cornell.mannlib.vitro.webapp.controller.freemarker;

import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.InputStream;
import java.io.StringWriter;
import java.util.HashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.stream.Collectors;

import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServletResponse;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.apache.jena.query.Query;
import org.apache.jena.query.QueryExecution;
import org.apache.jena.query.QueryExecutionFactory;
import org.apache.jena.query.QueryFactory;
import org.apache.jena.query.QuerySolution;
import org.apache.jena.query.QuerySolutionMap;
import org.apache.jena.query.ResultSet;
import org.apache.jena.rdf.model.Model;
import org.apache.jena.rdf.model.ModelFactory;
import org.apache.jena.rdf.model.ResourceFactory;
import org.apache.jena.shared.Lock;

import edu.cornell.mannlib.vitro.webapp.auth.permissions.SimplePermission;
import edu.cornell.mannlib.vitro.webapp.auth.requestedAction.AuthorizationRequest;
import edu.cornell.mannlib.vitro.webapp.beans.Individual;
import edu.cornell.mannlib.vitro.webapp.controller.VitroRequest;
import edu.cornell.mannlib.vitro.webapp.controller.freemarker.responsevalues.RedirectResponseValues;
import edu.cornell.mannlib.vitro.webapp.controller.freemarker.responsevalues.ResponseValues;
import edu.cornell.mannlib.vitro.webapp.controller.freemarker.responsevalues.TemplateResponseValues;
import static edu.cornell.mannlib.vitro.webapp.dao.DisplayVocabulary.HAS_DELETE_QUERY;

import edu.cornell.mannlib.vitro.webapp.dao.jena.event.BulkUpdateEvent;
import edu.cornell.mannlib.vitro.webapp.modelaccess.ModelAccess;
import edu.cornell.mannlib.vitro.webapp.modelaccess.ModelNames;
import edu.cornell.mannlib.vitro.webapp.rdfservice.ChangeSet;
import edu.cornell.mannlib.vitro.webapp.rdfservice.RDFService;
import edu.cornell.mannlib.vitro.webapp.rdfservice.impl.RDFServiceUtils;

@WebServlet(name = "DeleteIndividualController", urlPatterns = "/deleteIndividualController")
public class DeleteIndividualController extends FreemarkerHttpServlet {

private static final String INDIVIDUAL_URI = "individualUri";
private static final long serialVersionUID = 1L;
private static final Log log = LogFactory.getLog(DeleteIndividualController.class);
private static final boolean BEGIN = true;
private static final boolean END = !BEGIN;

private static String queryForDeleteQuery = ""
+ "SELECT ?deleteQueryText WHERE { "
+ "?associatedUri <" + HAS_DELETE_QUERY + "> ?deleteQueryText ."
+ "}";

private static final String DEFAULT_DELETE_QUERY_TEXT = ""
+ "CONSTRUCT { ?individualUri ?p1 ?o1 . ?s2 ?p2 ?individualUri . } "
+ "WHERE {"
+ " { ?individualUri ?p1 ?o1 . } UNION { ?s2 ?p2 ?individualUri. } "
+ "}";

@Override
protected AuthorizationRequest requiredActions(VitroRequest vreq) {
return SimplePermission.DO_FRONT_END_EDITING.ACTION;
}

protected ResponseValues processRequest(VitroRequest vreq) {
String errorMessage = handleErrors(vreq);
if (!errorMessage.isEmpty()) {
return prepareErrorMessage(errorMessage);
}
String individualUri = vreq.getParameter(INDIVIDUAL_URI);
List<String> types = getObjectMostSpecificTypes(individualUri, vreq);
Model displayModel = vreq.getDisplayModel();

String deleteQueryText = getDeleteQueryForTypes(types, displayModel);
Model toRemove = getIndividualsToDelete(individualUri, deleteQueryText, vreq);
if (toRemove.size() > 0) {
deleteIndividuals(toRemove, vreq);
}
String redirectUrl = getRedirectUrl(vreq);

return new RedirectResponseValues(redirectUrl, HttpServletResponse.SC_SEE_OTHER);
}

private String getRedirectUrl(VitroRequest vreq) {
String redirectUrl = vreq.getParameter("redirectUrl");
if (redirectUrl != null) {
return redirectUrl;
}
return "/";
}

private TemplateResponseValues prepareErrorMessage(String errorMessage) {
HashMap<String, Object> map = new HashMap<String, Object>();
map.put("errorMessage", errorMessage);
return new TemplateResponseValues("error-message.ftl", map);
}

private String handleErrors(VitroRequest vreq) {
String uri = vreq.getParameter(INDIVIDUAL_URI);
if (uri == null) {
return "Individual uri is null. No object to delete.";
}
if (uri.contains("<") || uri.contains(">")) {
return "Individual IRI shouldn't contain '<' or '>";
}
return "";
}

private static String getDeleteQueryForTypes(List<String> types, Model displayModel) {
String deleteQueryText = DEFAULT_DELETE_QUERY_TEXT;
String foundType = "";
for ( String type: types) {
Query queryForTypeSpecificDeleteQuery = QueryFactory.create(queryForDeleteQuery);
QuerySolutionMap initialBindings = new QuerySolutionMap();
initialBindings.add("associatedURI", ResourceFactory.createResource(type));
displayModel.enterCriticalSection(Lock.READ);
try {
QueryExecution qexec = QueryExecutionFactory.create(queryForTypeSpecificDeleteQuery, displayModel,
initialBindings);
try {
ResultSet results = qexec.execSelect();
if (results.hasNext()) {
QuerySolution solution = results.nextSolution();
deleteQueryText = solution.get("deleteQueryText").toString();
foundType = type;
}
} finally {
qexec.close();
}
} finally {
displayModel.leaveCriticalSection();
}
if (!foundType.isEmpty()) {
break;
}
}

if (!foundType.isEmpty()) {
log.debug("For " + foundType + " found delete query \n" + deleteQueryText);
if (!deleteQueryText.contains(INDIVIDUAL_URI)){
log.error("Safety check failed. Delete query text should contain " + INDIVIDUAL_URI + ", "
+ "but it didn't. To prevent bad consequences query was rejected.");
log.error("Delete query which caused the error: \n" + deleteQueryText);
deleteQueryText = DEFAULT_DELETE_QUERY_TEXT;
}
} else {
log.debug("For most specific types: " + types.stream().collect(Collectors.joining(",")) + " no delete query was found. Using default query \n" + deleteQueryText);
}
return deleteQueryText;
}

private List<String> getObjectMostSpecificTypes(String individualUri, VitroRequest vreq) {
List<String> types = new LinkedList<String>();
Individual individual = vreq.getWebappDaoFactory().getIndividualDao().getIndividualByURI(individualUri);
if (individual != null) {
types = individual.getMostSpecificTypeURIs();
}
if (types.isEmpty()) {
log.error("Failed to get most specific type for individual Uri " + individualUri);
}
return types;
}

private Model getIndividualsToDelete(String targetIndividual, String deleteQuery, VitroRequest vreq) {
try {
Query queryForTypeSpecificDeleteQuery = QueryFactory.create(deleteQuery);
QuerySolutionMap bindings = new QuerySolutionMap();
bindings.add(INDIVIDUAL_URI, ResourceFactory.createResource(targetIndividual));
Model ontModel = ModelAccess.on(vreq).getOntModelSelector().getABoxModel();
QueryExecution qexec = QueryExecutionFactory.create(queryForTypeSpecificDeleteQuery, ontModel, bindings);
Model results = qexec.execConstruct();
return results;

} catch (Exception e) {
log.error("Query raised an error \n" + deleteQuery);
log.error(e, e);
}
return ModelFactory.createDefaultModel();
}

private void deleteIndividuals(Model model, VitroRequest vreq) {
RDFService rdfService = vreq.getRDFService();
ChangeSet cs = makeChangeSet(rdfService);
try {
ByteArrayOutputStream out = new ByteArrayOutputStream();
model.write(out, "N3");
InputStream in = new ByteArrayInputStream(out.toByteArray());
cs.addRemoval(in, RDFServiceUtils.getSerializationFormatFromJenaString("N3"), ModelNames.ABOX_ASSERTIONS);
rdfService.changeSetUpdate(cs);
} catch (Exception e) {
StringWriter sw = new StringWriter();
model.write(sw, "N3");
log.error("Got " + e.getClass().getSimpleName() + " while removing\n" + sw.toString());
log.error(e,e);
throw new RuntimeException(e);
}
}

private ChangeSet makeChangeSet(RDFService rdfService) {
ChangeSet cs = rdfService.manufactureChangeSet();
cs.addPreChangeEvent(new BulkUpdateEvent(null, BEGIN));
cs.addPostChangeEvent(new BulkUpdateEvent(null, END));
return cs;
}

}
Expand Up @@ -53,6 +53,8 @@ public class DisplayVocabulary {
//specific case for internal class, value is true or false
public static final String RESTRICT_RESULTS_BY_INTERNAL = NS + "restrictResultsByInternalClass";

public static final String HAS_DELETE_QUERY = NS + "hasDeleteQuery";


/* Data Properties */
public static final DatatypeProperty URL_MAPPING = m_model.createDatatypeProperty(NS + "urlMapping");
Expand Down
Expand Up @@ -2,8 +2,6 @@

package edu.cornell.mannlib.vitro.webapp.edit.n3editing.configuration.generators;

import java.util.HashMap;

import javax.servlet.http.HttpSession;

import org.apache.commons.logging.Log;
Expand All @@ -21,14 +19,16 @@
*/
public class DefaultDeleteGenerator extends BaseEditConfigurationGenerator implements EditConfigurationGenerator {

private Log log = LogFactory.getLog(DefaultObjectPropertyFormGenerator.class);
private static final Log log = LogFactory.getLog(DefaultObjectPropertyFormGenerator.class);
private static final String PROPERTY_TEMPLATE = "confirmDeletePropertyForm.ftl";
private static final String INDIVIDUAL_TEMPLATE = "confirmDeleteIndividualForm.ftl";

private String subjectUri = null;
private String predicateUri = null;
private String objectUri = null;
private Integer dataHash = 0;
private DataPropertyStatement dps = null;
private String dataLiteral = null;
private String template = "confirmDeletePropertyForm.ftl";


//In this case, simply return the edit configuration currently saved in session
//Since this is forwarding from another form, an edit configuration should already exist in session
Expand All @@ -43,17 +43,37 @@ public EditConfigurationVTwo getEditConfiguration(VitroRequest vreq,
if(editConfiguration == null) {
editConfiguration = setupEditConfiguration(vreq, session);
}
editConfiguration.setTemplate(template);
//prepare update?
prepare(vreq, editConfiguration);
if (editConfiguration.getPredicateUri() == null && editConfiguration.getSubjectUri() == null) {
editConfiguration.setTemplate(INDIVIDUAL_TEMPLATE);
addDeleteParams(vreq, editConfiguration);
}else {
editConfiguration.setTemplate(PROPERTY_TEMPLATE);
}
return editConfiguration;
}

private void addDeleteParams(VitroRequest vreq, EditConfigurationVTwo editConfiguration) {
String redirectUrl = vreq.getParameter("redirectUrl");
if (redirectUrl != null) {
editConfiguration.addFormSpecificData("redirectUrl", redirectUrl);
}
String individualName = vreq.getParameter("individualName");
if (redirectUrl != null) {
editConfiguration.addFormSpecificData("individualName", individualName);
}
String individualType = vreq.getParameter("individualType");
if (redirectUrl != null) {
editConfiguration.addFormSpecificData("individualType", individualType);
}
}

private EditConfigurationVTwo setupEditConfiguration(VitroRequest vreq, HttpSession session) {
EditConfigurationVTwo editConfiguration = new EditConfigurationVTwo();
initProcessParameters(vreq, session, editConfiguration);
//set edit key for this as well
editConfiguration.setEditKey(editConfiguration.newEditKey(session));
editConfiguration.setEditKey(EditConfigurationVTwo.newEditKey(session));
return editConfiguration;

}
Expand Down
Expand Up @@ -78,6 +78,9 @@ protected AuthorizationRequest requiredActions(VitroRequest vreq) {
} else if(MANAGE_MENUS_FORM.equals(vreq.getParameter("editForm"))) {
return SimplePermission.MANAGE_MENUS.ACTION;
}
if (isIndividualDeletion(vreq)) {
return SimplePermission.DO_BACK_END_EDITING.ACTION;
}
// Check if this statement can be edited here and return unauthorized if not
String subjectUri = EditConfigurationUtils.getSubjectUri(vreq);
String predicateUri = EditConfigurationUtils.getPredicateUri(vreq);
Expand Down Expand Up @@ -106,6 +109,16 @@ protected AuthorizationRequest requiredActions(VitroRequest vreq) {
return isAuthorized? SimplePermission.DO_FRONT_END_EDITING.ACTION: AuthorizationRequest.UNAUTHORIZED;
}

private boolean isIndividualDeletion(VitroRequest vreq) {
String subjectUri = EditConfigurationUtils.getSubjectUri(vreq);
String predicateUri = EditConfigurationUtils.getPredicateUri(vreq);
String objectUri = EditConfigurationUtils.getObjectUri(vreq);
if (objectUri != null && subjectUri == null && predicateUri == null && isDeleteForm(vreq)) {
return true;
}
return false;
}

@Override
protected ResponseValues processRequest(VitroRequest vreq) {

Expand Down Expand Up @@ -363,7 +376,7 @@ private boolean isErrorCondition(VitroRequest vreq) {
String predicateUri = EditConfigurationUtils.getPredicateUri(vreq);
String formParam = getFormParam(vreq);
//if no form parameter, then predicate uri and subject uri must both be populated
if (formParam == null || "".equals(formParam)) {
if ((formParam == null || "".equals(formParam)) && !isDeleteForm(vreq)) {
if ((predicateUri == null || predicateUri.trim().length() == 0)) {
return true;
}
Expand Down
Expand Up @@ -718,6 +718,10 @@ public String getDeleteProcessingUrl() {
return vreq.getContextPath() + "/deletePropertyController";
}

public String getDeleteIndividualProcessingUrl() {
return vreq.getContextPath() + "/deleteIndividualController";
}

//TODO: Check if this logic is correct and delete prohibited does not expect a specific value
public boolean isDeleteProhibited() {
String deleteProhibited = vreq.getParameter("deleteProhibited");
Expand Down
Expand Up @@ -7,6 +7,7 @@
import static edu.cornell.mannlib.vitro.webapp.auth.requestedAction.RequestedAction.SOME_URI;

import java.util.Collection;
import java.util.Iterator;
import java.util.List;
import java.util.Map;

Expand All @@ -27,6 +28,7 @@
import edu.cornell.mannlib.vitro.webapp.config.ConfigurationProperties;
import edu.cornell.mannlib.vitro.webapp.controller.VitroRequest;
import edu.cornell.mannlib.vitro.webapp.controller.freemarker.UrlBuilder;
import edu.cornell.mannlib.vitro.webapp.controller.freemarker.UrlBuilder.ParamMap;
import edu.cornell.mannlib.vitro.webapp.controller.freemarker.UrlBuilder.Route;
import edu.cornell.mannlib.vitro.webapp.dao.ObjectPropertyStatementDao;
import edu.cornell.mannlib.vitro.webapp.dao.VClassDao;
Expand All @@ -37,6 +39,7 @@
public abstract class BaseIndividualTemplateModel extends BaseTemplateModel {

private static final Log log = LogFactory.getLog(BaseIndividualTemplateModel.class);
private static final String EDIT_PATH = "editRequestDispatch";

protected final Individual individual;
protected final LoginStatusBean loginStatusBean;
Expand Down Expand Up @@ -148,6 +151,22 @@ public NameStatementTemplateModel getNameStatement() {
public String getName() {
return individual.getName();
}

public String getDeleteUrl() {
Collection<String> types = getMostSpecificTypes();
ParamMap params = new ParamMap(
"objectUri", individual.getURI(),
"cmd", "delete",
"individualName",getNameStatement().getValue()
);
Iterator<String> typesIterator = types.iterator();
if (types.iterator().hasNext()) {
String type = typesIterator.next();
params.put("individualType", type);
}

return UrlBuilder.getUrl(EDIT_PATH, params);
}

public Collection<String> getMostSpecificTypes() {
ObjectPropertyStatementDao opsDao = vreq.getWebappDaoFactory().getObjectPropertyStatementDao();
Expand Down
Expand Up @@ -128,6 +128,7 @@
<owl:ObjectProperty rdf:about="&display;restrictResultsByClass"/>
<owl:ObjectProperty rdf:about="&display;getIndividualsForClass"/>
<owl:ObjectProperty rdf:about="&display;hasDataGetter"/>
<owl:DataProperty rdf:about="&display;hasDeleteQuery"/>
<owl:ObjectProperty rdf:about="&display;requiresAction">

</owl:ObjectProperty>
Expand Down

0 comments on commit ff285fb

Please sign in to comment.