diff --git a/api/src/main/java/edu/cornell/mannlib/vitro/webapp/controller/freemarker/DeleteIndividualController.java b/api/src/main/java/edu/cornell/mannlib/vitro/webapp/controller/freemarker/DeleteIndividualController.java new file mode 100644 index 0000000000..6c05bc3d89 --- /dev/null +++ b/api/src/main/java/edu/cornell/mannlib/vitro/webapp/controller/freemarker/DeleteIndividualController.java @@ -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 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 map = new HashMap(); + 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 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 getObjectMostSpecificTypes(String individualUri, VitroRequest vreq) { + List types = new LinkedList(); + 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; + } + +} diff --git a/api/src/main/java/edu/cornell/mannlib/vitro/webapp/dao/DisplayVocabulary.java b/api/src/main/java/edu/cornell/mannlib/vitro/webapp/dao/DisplayVocabulary.java index 5fb4e3073f..964726abcb 100644 --- a/api/src/main/java/edu/cornell/mannlib/vitro/webapp/dao/DisplayVocabulary.java +++ b/api/src/main/java/edu/cornell/mannlib/vitro/webapp/dao/DisplayVocabulary.java @@ -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"); diff --git a/api/src/main/java/edu/cornell/mannlib/vitro/webapp/edit/n3editing/configuration/generators/DefaultDeleteGenerator.java b/api/src/main/java/edu/cornell/mannlib/vitro/webapp/edit/n3editing/configuration/generators/DefaultDeleteGenerator.java index 7b77786d44..7724688dc3 100644 --- a/api/src/main/java/edu/cornell/mannlib/vitro/webapp/edit/n3editing/configuration/generators/DefaultDeleteGenerator.java +++ b/api/src/main/java/edu/cornell/mannlib/vitro/webapp/edit/n3editing/configuration/generators/DefaultDeleteGenerator.java @@ -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; @@ -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 @@ -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; } diff --git a/api/src/main/java/edu/cornell/mannlib/vitro/webapp/edit/n3editing/controller/EditRequestDispatchController.java b/api/src/main/java/edu/cornell/mannlib/vitro/webapp/edit/n3editing/controller/EditRequestDispatchController.java index 16b1122fbe..c33b70279a 100644 --- a/api/src/main/java/edu/cornell/mannlib/vitro/webapp/edit/n3editing/controller/EditRequestDispatchController.java +++ b/api/src/main/java/edu/cornell/mannlib/vitro/webapp/edit/n3editing/controller/EditRequestDispatchController.java @@ -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); @@ -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) { @@ -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; } diff --git a/api/src/main/java/edu/cornell/mannlib/vitro/webapp/web/templatemodels/edit/EditConfigurationTemplateModel.java b/api/src/main/java/edu/cornell/mannlib/vitro/webapp/web/templatemodels/edit/EditConfigurationTemplateModel.java index c3bd701dd0..577229162e 100644 --- a/api/src/main/java/edu/cornell/mannlib/vitro/webapp/web/templatemodels/edit/EditConfigurationTemplateModel.java +++ b/api/src/main/java/edu/cornell/mannlib/vitro/webapp/web/templatemodels/edit/EditConfigurationTemplateModel.java @@ -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"); diff --git a/api/src/main/java/edu/cornell/mannlib/vitro/webapp/web/templatemodels/individual/BaseIndividualTemplateModel.java b/api/src/main/java/edu/cornell/mannlib/vitro/webapp/web/templatemodels/individual/BaseIndividualTemplateModel.java index 8c25395ffe..6ebd52ebc1 100644 --- a/api/src/main/java/edu/cornell/mannlib/vitro/webapp/web/templatemodels/individual/BaseIndividualTemplateModel.java +++ b/api/src/main/java/edu/cornell/mannlib/vitro/webapp/web/templatemodels/individual/BaseIndividualTemplateModel.java @@ -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; @@ -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; @@ -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; @@ -148,6 +151,22 @@ public NameStatementTemplateModel getNameStatement() { public String getName() { return individual.getName(); } + + public String getDeleteUrl() { + Collection types = getMostSpecificTypes(); + ParamMap params = new ParamMap( + "objectUri", individual.getURI(), + "cmd", "delete", + "individualName",getNameStatement().getValue() + ); + Iterator typesIterator = types.iterator(); + if (types.iterator().hasNext()) { + String type = typesIterator.next(); + params.put("individualType", type); + } + + return UrlBuilder.getUrl(EDIT_PATH, params); + } public Collection getMostSpecificTypes() { ObjectPropertyStatementDao opsDao = vreq.getWebappDaoFactory().getObjectPropertyStatementDao(); diff --git a/home/src/main/resources/rdf/display/firsttime/application.owl b/home/src/main/resources/rdf/display/firsttime/application.owl index 02dbd57368..3a48d3ef57 100644 --- a/home/src/main/resources/rdf/display/firsttime/application.owl +++ b/home/src/main/resources/rdf/display/firsttime/application.owl @@ -128,6 +128,7 @@ + diff --git a/home/src/main/resources/rdf/displayTbox/everytime/displayTBOX.n3 b/home/src/main/resources/rdf/displayTbox/everytime/displayTBOX.n3 index d5fcf9ec28..07478b7717 100644 --- a/home/src/main/resources/rdf/displayTbox/everytime/displayTBOX.n3 +++ b/home/src/main/resources/rdf/displayTbox/everytime/displayTBOX.n3 @@ -204,6 +204,9 @@ vitro:additionalLink display:hasElement a owl:ObjectProperty . +display:hasDeleteQuery + a owl:DataProperty . + display:excludeClass a owl:ObjectProperty . diff --git a/webapp/src/main/webapp/templates/freemarker/body/individual/individual-vitro.ftl b/webapp/src/main/webapp/templates/freemarker/body/individual/individual-vitro.ftl index 3a99a1c39c..b49321de58 100644 --- a/webapp/src/main/webapp/templates/freemarker/body/individual/individual-vitro.ftl +++ b/webapp/src/main/webapp/templates/freemarker/body/individual/individual-vitro.ftl @@ -49,7 +49,9 @@

<#-- Label --> <@p.label individual editable labelCount localesCount languageCount/> - + <#if editable> + <@p.deleteIndividualLink individual /> + <#-- Most-specific types --> <@p.mostSpecificTypes individual /> uri icon diff --git a/webapp/src/main/webapp/templates/freemarker/edit/forms/confirmDeleteIndividualForm.ftl b/webapp/src/main/webapp/templates/freemarker/edit/forms/confirmDeleteIndividualForm.ftl new file mode 100644 index 0000000000..c22660d8e4 --- /dev/null +++ b/webapp/src/main/webapp/templates/freemarker/edit/forms/confirmDeleteIndividualForm.ftl @@ -0,0 +1,33 @@ +<#-- $This file is distributed under the terms of the license in LICENSE$ --> +<#if editConfiguration.pageData.redirectUrl??> + <#assign redirectUrl = editConfiguration.pageData.redirectUrl /> +<#else> + <#assign redirectUrl = "/" /> + +<#if editConfiguration.pageData.individualName??> + <#assign individualName = editConfiguration.pageData.individualName /> + +<#if editConfiguration.pageData.individualType??> + <#assign individualType = editConfiguration.pageData.individualType /> + + +
+

${i18n().confirm_individual_deletion}

+ + + +

+ <#if individualType??> + ${individualType} + + <#if individualName??> + ${individualName} + +

+
+

+ + or + ${i18n().cancel_link} +

+
diff --git a/webapp/src/main/webapp/templates/freemarker/lib/lib-properties.ftl b/webapp/src/main/webapp/templates/freemarker/lib/lib-properties.ftl index 298a63132d..4574ec6f78 100644 --- a/webapp/src/main/webapp/templates/freemarker/lib/lib-properties.ftl +++ b/webapp/src/main/webapp/templates/freemarker/lib/lib-properties.ftl @@ -203,6 +203,16 @@ name will be used as the label. --> ${i18n().edit_entry} +<#macro deleteIndividualLink individual redirectUrl="/"> + <#local url = individual.deleteUrl + "&redirectUrl=" + "${redirectUrl}"> + <@showDeleteIndividualLink url /> + + + +<#macro showDeleteIndividualLink url> + ${i18n().delete_entry} + + <#macro deleteLink propertyLocalName propertyName statement rangeUri=""> <#local url = statement.deleteUrl> <#if url?has_content>