diff --git a/api/src/main/java/edu/cornell/mannlib/vitro/webapp/beans/UserAccount.java b/api/src/main/java/edu/cornell/mannlib/vitro/webapp/beans/UserAccount.java index 7b9d49792..7b5581885 100644 --- a/api/src/main/java/edu/cornell/mannlib/vitro/webapp/beans/UserAccount.java +++ b/api/src/main/java/edu/cornell/mannlib/vitro/webapp/beans/UserAccount.java @@ -7,17 +7,11 @@ import java.util.Collections; import java.util.HashSet; import java.util.Set; -import edu.cornell.mannlib.vitro.webapp.controller.authenticate.Authenticator; +import org.apache.commons.lang3.RandomStringUtils; /** * Information about the account of a user. URI, email, password, etc. * - * The "password link expires hash" is just a string that is derived from the - * value in the passwordLinkExpires field. It doesn't have to be a hash, and - * there is no need for it to be cryptographic, but it seems embarrassing to - * just send the value as a clear string. There is no real need for security - * here, except that a brute force attack would allow someone to change the - * password on an account that they know has a password change pending. */ public class UserAccount { public static final int MIN_PASSWORD_LENGTH = 6; @@ -52,6 +46,7 @@ public class UserAccount { private String md5Password = ""; // Never null. private String oldPassword = ""; // Never null. private long passwordLinkExpires = 0L; // Never negative. + private String emailKey = ""; private boolean passwordChangeRequired = false; private int loginCount = 0; // Never negative. @@ -133,15 +128,27 @@ public class UserAccount { return passwordLinkExpires; } - public String getPasswordLinkExpiresHash() { - return limitStringLength(8, Authenticator.applyArgon2iEncoding(String - .valueOf(passwordLinkExpires))); - } - public void setPasswordLinkExpires(long passwordLinkExpires) { this.passwordLinkExpires = Math.max(0, passwordLinkExpires); } + public void generateEmailKey() { + boolean useLetters = true; + boolean useNumbers = true; + int length = 64; + emailKey = RandomStringUtils.random(length, useLetters, useNumbers); + } + + public void setEmailKey(String emailKey) { + if (emailKey != null) { + this.emailKey = emailKey; + } + } + + public String getEmailKey() { + return emailKey; + } + public boolean isPasswordChangeRequired() { return passwordChangeRequired; } @@ -247,6 +254,7 @@ public class UserAccount { + (", oldPassword=" + oldPassword) + (", argon2password=" + argon2Password) + (", passwordLinkExpires=" + passwordLinkExpires) + + (", emailKey =" + emailKey) + (", passwordChangeRequired=" + passwordChangeRequired) + (", externalAuthOnly=" + externalAuthOnly) + (", loginCount=" + loginCount) + (", status=" + status) diff --git a/api/src/main/java/edu/cornell/mannlib/vitro/webapp/controller/accounts/UserAccountsSelector.java b/api/src/main/java/edu/cornell/mannlib/vitro/webapp/controller/accounts/UserAccountsSelector.java index 6caadf24c..d878ae315 100644 --- a/api/src/main/java/edu/cornell/mannlib/vitro/webapp/controller/accounts/UserAccountsSelector.java +++ b/api/src/main/java/edu/cornell/mannlib/vitro/webapp/controller/accounts/UserAccountsSelector.java @@ -249,6 +249,7 @@ public class UserAccountsSelector { user.setMd5Password(ifLiteralPresent(solution, "md5pwd", "")); user.setArgon2Password(ifLiteralPresent(solution, "a2pwd", "")); user.setPasswordLinkExpires(ifLongPresent(solution, "expire", 0L)); + user.setEmailKey(ifLiteralPresent(solution, "emailKey", "")); user.setLoginCount(ifIntPresent(solution, "count", 0)); user.setLastLoginTime(ifLongPresent(solution, "lastLogin", 0)); user.setStatus(parseStatus(solution, "status", null)); diff --git a/api/src/main/java/edu/cornell/mannlib/vitro/webapp/controller/accounts/admin/UserAccountsAddPage.java b/api/src/main/java/edu/cornell/mannlib/vitro/webapp/controller/accounts/admin/UserAccountsAddPage.java index bdbc5dbce..7fc4181da 100644 --- a/api/src/main/java/edu/cornell/mannlib/vitro/webapp/controller/accounts/admin/UserAccountsAddPage.java +++ b/api/src/main/java/edu/cornell/mannlib/vitro/webapp/controller/accounts/admin/UserAccountsAddPage.java @@ -156,6 +156,7 @@ public class UserAccountsAddPage extends UserAccountsPage { u.setOldPassword(""); u.setPasswordChangeRequired(false); u.setPasswordLinkExpires(0); + u.setEmailKey(""); u.setLoginCount(0); u.setLastLoginTime(0L); u.setStatus(Status.INACTIVE); diff --git a/api/src/main/java/edu/cornell/mannlib/vitro/webapp/controller/accounts/admin/UserAccountsAddPageStrategy.java b/api/src/main/java/edu/cornell/mannlib/vitro/webapp/controller/accounts/admin/UserAccountsAddPageStrategy.java index 307ccf9f6..036d314e2 100644 --- a/api/src/main/java/edu/cornell/mannlib/vitro/webapp/controller/accounts/admin/UserAccountsAddPageStrategy.java +++ b/api/src/main/java/edu/cornell/mannlib/vitro/webapp/controller/accounts/admin/UserAccountsAddPageStrategy.java @@ -84,6 +84,7 @@ public abstract class UserAccountsAddPageStrategy extends UserAccountsPage { u.setStatus(Status.ACTIVE); } else { u.setPasswordLinkExpires(figureExpirationDate().getTime()); + u.generateEmailKey(); u.setStatus(Status.INACTIVE); } } @@ -119,10 +120,8 @@ public abstract class UserAccountsAddPageStrategy extends UserAccountsPage { private String buildCreatePasswordLink() { try { String email = page.getAddedAccount().getEmailAddress(); - String hash = page.getAddedAccount() - .getPasswordLinkExpiresHash(); - String relativeUrl = UrlBuilder.getUrl(CREATE_PASSWORD_URL, - "user", email, "key", hash); + String key = page.getAddedAccount().getEmailKey(); + String relativeUrl = UrlBuilder.getUrl(CREATE_PASSWORD_URL, "user", email, "key", key); URL context = new URL(vreq.getRequestURL().toString()); URL url = new URL(context, relativeUrl); diff --git a/api/src/main/java/edu/cornell/mannlib/vitro/webapp/controller/accounts/admin/UserAccountsEditPage.java b/api/src/main/java/edu/cornell/mannlib/vitro/webapp/controller/accounts/admin/UserAccountsEditPage.java index 42dd4102d..13e1ab0dd 100644 --- a/api/src/main/java/edu/cornell/mannlib/vitro/webapp/controller/accounts/admin/UserAccountsEditPage.java +++ b/api/src/main/java/edu/cornell/mannlib/vitro/webapp/controller/accounts/admin/UserAccountsEditPage.java @@ -274,6 +274,7 @@ public class UserAccountsEditPage extends UserAccountsPage { userAccount.setOldPassword(""); userAccount.setPasswordChangeRequired(false); userAccount.setPasswordLinkExpires(0L); + userAccount.setEmailKey(""); } if (isRootUser()) { diff --git a/api/src/main/java/edu/cornell/mannlib/vitro/webapp/controller/accounts/admin/UserAccountsEditPageStrategy.java b/api/src/main/java/edu/cornell/mannlib/vitro/webapp/controller/accounts/admin/UserAccountsEditPageStrategy.java index 708f11e66..d9ee8aa14 100644 --- a/api/src/main/java/edu/cornell/mannlib/vitro/webapp/controller/accounts/admin/UserAccountsEditPageStrategy.java +++ b/api/src/main/java/edu/cornell/mannlib/vitro/webapp/controller/accounts/admin/UserAccountsEditPageStrategy.java @@ -82,6 +82,7 @@ public abstract class UserAccountsEditPageStrategy extends UserAccountsPage { protected void setAdditionalProperties(UserAccount u) { if (resetPassword && !page.isExternalAuthOnly()) { u.setPasswordLinkExpires(figureExpirationDate().getTime()); + u.generateEmailKey(); } } @@ -121,10 +122,8 @@ public abstract class UserAccountsEditPageStrategy extends UserAccountsPage { private String buildResetPasswordLink() { try { String email = page.getUpdatedAccount().getEmailAddress(); - String hash = page.getUpdatedAccount() - .getPasswordLinkExpiresHash(); - String relativeUrl = UrlBuilder.getUrl(RESET_PASSWORD_URL, - "user", email, "key", hash); + String key = page.getUpdatedAccount().getEmailKey(); + String relativeUrl = UrlBuilder.getUrl(RESET_PASSWORD_URL, "user", email, "key", key); URL context = new URL(vreq.getRequestURL().toString()); URL url = new URL(context, relativeUrl); diff --git a/api/src/main/java/edu/cornell/mannlib/vitro/webapp/controller/accounts/user/UserAccountsCreatePasswordPage.java b/api/src/main/java/edu/cornell/mannlib/vitro/webapp/controller/accounts/user/UserAccountsCreatePasswordPage.java index b9515d4c7..68daa2d67 100644 --- a/api/src/main/java/edu/cornell/mannlib/vitro/webapp/controller/accounts/user/UserAccountsCreatePasswordPage.java +++ b/api/src/main/java/edu/cornell/mannlib/vitro/webapp/controller/accounts/user/UserAccountsCreatePasswordPage.java @@ -36,6 +36,7 @@ public class UserAccountsCreatePasswordPage extends userAccount.setArgon2Password(Authenticator.applyArgon2iEncoding(newPassword)); userAccount.setMd5Password(""); userAccount.setPasswordLinkExpires(0L); + userAccount.setEmailKey(""); userAccount.setPasswordChangeRequired(false); userAccount.setStatus(Status.ACTIVE); userAccountsDao.updateUserAccount(userAccount); @@ -53,6 +54,11 @@ public class UserAccountsCreatePasswordPage extends protected String passwordChangeNotPendingMessage() { return i18n.text("account_already_activated", userEmail); } + + @Override + protected String passwordChangeInavlidKeyMessage() { + return i18n.text("password_change_invalid_key", userEmail); + } @Override protected String templateName() { diff --git a/api/src/main/java/edu/cornell/mannlib/vitro/webapp/controller/accounts/user/UserAccountsFirstTimeExternalPage.java b/api/src/main/java/edu/cornell/mannlib/vitro/webapp/controller/accounts/user/UserAccountsFirstTimeExternalPage.java index fc11f665d..ffb34c754 100644 --- a/api/src/main/java/edu/cornell/mannlib/vitro/webapp/controller/accounts/user/UserAccountsFirstTimeExternalPage.java +++ b/api/src/main/java/edu/cornell/mannlib/vitro/webapp/controller/accounts/user/UserAccountsFirstTimeExternalPage.java @@ -195,6 +195,7 @@ public class UserAccountsFirstTimeExternalPage extends UserAccountsPage { u.setExternalAuthId(externalAuthId); u.setPasswordChangeRequired(false); u.setPasswordLinkExpires(0); + u.setEmailKey(""); u.setExternalAuthOnly(true); u.setLoginCount(0); u.setStatus(Status.ACTIVE); diff --git a/api/src/main/java/edu/cornell/mannlib/vitro/webapp/controller/accounts/user/UserAccountsMyAccountPageStrategy.java b/api/src/main/java/edu/cornell/mannlib/vitro/webapp/controller/accounts/user/UserAccountsMyAccountPageStrategy.java index 057098eea..ca895cab8 100644 --- a/api/src/main/java/edu/cornell/mannlib/vitro/webapp/controller/accounts/user/UserAccountsMyAccountPageStrategy.java +++ b/api/src/main/java/edu/cornell/mannlib/vitro/webapp/controller/accounts/user/UserAccountsMyAccountPageStrategy.java @@ -159,6 +159,7 @@ public abstract class UserAccountsMyAccountPageStrategy extends userAccount.setMd5Password(""); userAccount.setPasswordChangeRequired(false); userAccount.setPasswordLinkExpires(0L); + userAccount.setEmailKey(""); } } diff --git a/api/src/main/java/edu/cornell/mannlib/vitro/webapp/controller/accounts/user/UserAccountsPasswordBasePage.java b/api/src/main/java/edu/cornell/mannlib/vitro/webapp/controller/accounts/user/UserAccountsPasswordBasePage.java index 3923c17b2..d4cc56f03 100644 --- a/api/src/main/java/edu/cornell/mannlib/vitro/webapp/controller/accounts/user/UserAccountsPasswordBasePage.java +++ b/api/src/main/java/edu/cornell/mannlib/vitro/webapp/controller/accounts/user/UserAccountsPasswordBasePage.java @@ -103,12 +103,12 @@ public abstract class UserAccountsPasswordBasePage extends UserAccountsPage { return; } - String expectedKey = userAccount.getPasswordLinkExpiresHash(); - if (!key.equals(expectedKey)) { + String expectedKey = userAccount.getEmailKey(); + if (key.isEmpty() || !key.equals(expectedKey)) { log.warn("Password request for '" + userEmail + "' is bogus: key (" + key + ") doesn't match expected key (" + expectedKey + ")"); - bogusMessage = passwordChangeNotPendingMessage(); + bogusMessage = passwordChangeInavlidKeyMessage(); return; } @@ -153,7 +153,7 @@ public abstract class UserAccountsPasswordBasePage extends UserAccountsPage { body.put("minimumLength", UserAccount.MIN_PASSWORD_LENGTH); body.put("maximumLength", UserAccount.MAX_PASSWORD_LENGTH); body.put("userAccount", userAccount); - body.put("key", userAccount.getPasswordLinkExpiresHash()); + body.put("key", userAccount.getEmailKey()); body.put("newPassword", newPassword); body.put("confirmPassword", confirmPassword); body.put("formUrls", buildUrlsMap()); @@ -176,6 +176,8 @@ public abstract class UserAccountsPasswordBasePage extends UserAccountsPage { protected abstract String alreadyLoggedInMessage(String currentUserEmail); protected abstract String passwordChangeNotPendingMessage(); + + protected abstract String passwordChangeInavlidKeyMessage(); protected abstract String templateName(); } diff --git a/api/src/main/java/edu/cornell/mannlib/vitro/webapp/controller/accounts/user/UserAccountsResetPasswordPage.java b/api/src/main/java/edu/cornell/mannlib/vitro/webapp/controller/accounts/user/UserAccountsResetPasswordPage.java index 712df1b40..f865cbe94 100644 --- a/api/src/main/java/edu/cornell/mannlib/vitro/webapp/controller/accounts/user/UserAccountsResetPasswordPage.java +++ b/api/src/main/java/edu/cornell/mannlib/vitro/webapp/controller/accounts/user/UserAccountsResetPasswordPage.java @@ -55,6 +55,11 @@ public class UserAccountsResetPasswordPage extends UserAccountsPasswordBasePage protected String passwordChangeNotPendingMessage() { return i18n.text("password_change_not_pending", userEmail); } + + @Override + protected String passwordChangeInavlidKeyMessage() { + return i18n.text("password_change_invalid_key", userEmail); + } @Override protected String templateName() { diff --git a/api/src/main/java/edu/cornell/mannlib/vitro/webapp/controller/authenticate/BasicAuthenticator.java b/api/src/main/java/edu/cornell/mannlib/vitro/webapp/controller/authenticate/BasicAuthenticator.java index f6f95081b..34fc6a01d 100644 --- a/api/src/main/java/edu/cornell/mannlib/vitro/webapp/controller/authenticate/BasicAuthenticator.java +++ b/api/src/main/java/edu/cornell/mannlib/vitro/webapp/controller/authenticate/BasicAuthenticator.java @@ -134,6 +134,7 @@ public class BasicAuthenticator extends Authenticator { userAccount.setMd5Password(""); userAccount.setPasswordChangeRequired(false); userAccount.setPasswordLinkExpires(0L); + userAccount.setEmailKey(""); getUserAccountsDao().updateUserAccount(userAccount); } 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 000000000..c6939fb77 --- /dev/null +++ b/api/src/main/java/edu/cornell/mannlib/vitro/webapp/controller/freemarker/DeleteIndividualController.java @@ -0,0 +1,201 @@ +package edu.cornell.mannlib.vitro.webapp.controller.freemarker; + +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.InputStream; +import java.nio.charset.StandardCharsets; +import java.util.HashMap; + +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.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.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 edu.cornell.mannlib.vitro.webapp.dao.DisplayVocabulary; +import edu.cornell.mannlib.vitro.webapp.dao.jena.QueryUtils; +import edu.cornell.mannlib.vitro.webapp.dao.jena.event.BulkUpdateEvent; +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.RDFServiceException; +import edu.cornell.mannlib.vitro.webapp.rdfservice.impl.RDFServiceUtils; + +@WebServlet(name="DeleteIndividualController",urlPatterns="/deleteIndividualController") +public class DeleteIndividualController extends FreemarkerHttpServlet{ + + private static final Log log = LogFactory.getLog(DeleteIndividualController.class); + private static final boolean BEGIN = true; + private static final boolean END = !BEGIN; + + private static String TYPE_QUERY_START = "" + + "PREFIX vitro: " + + "SELECT ?type " + + "WHERE" + + "{ <"; + private static String TYPE_QUERY_END = "> vitro:mostSpecificType ?type ." + + "}"; + private static String queryForDeleteQuery = + "PREFIX display: <" + DisplayVocabulary.DISPLAY_NS +"> \n" + + "SELECT ?deleteQueryText WHERE { ?associatedURI display:hasDeleteQuery ?deleteQueryText }"; + + private static final String DEFAULT_DELETE_QUERY_TEXT = "DESCRIBE ?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("individualUri"); + String type = getObjectMostSpecificType(individualUri, vreq); + Model displayModel = vreq.getDisplayModel(); + + String delteQueryText = getDeleteQueryForType(type, displayModel); + byte[] toRemove = getIndividualsToDelete(individualUri, delteQueryText, vreq); + if (toRemove.length > 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("individualUri"); + if ( uri == null) { + return "Individual uri is null. No object to delete."; + } + if (uri.contains(">")) { + return "Individual uri shouldn't contain >"; + } + return ""; + } + + private static String getDeleteQueryForType(String typeURI,Model displayModel) { + + String deleteQueryText = DEFAULT_DELETE_QUERY_TEXT; + + Query queryForTypeSpecificDeleteQuery = QueryFactory.create(queryForDeleteQuery); + + QuerySolutionMap initialBindings = new QuerySolutionMap(); + initialBindings.add("associatedURI", ResourceFactory.createResource( typeURI )); + + displayModel.enterCriticalSection(Lock.READ); + try{ + QueryExecution qexec = QueryExecutionFactory.create(queryForTypeSpecificDeleteQuery,displayModel,initialBindings ); + try{ + ResultSet results = qexec.execSelect(); + while (results.hasNext()) { + QuerySolution solution = results.nextSolution(); + deleteQueryText = solution.get("deleteQueryText").toString(); + } + }finally{ qexec.close(); } + }finally{ displayModel.leaveCriticalSection(); } + + if (!deleteQueryText.equals(DEFAULT_DELETE_QUERY_TEXT)) { + log.debug("For " + typeURI + " found delete query \n" + deleteQueryText); + } else { + log.debug("For " + typeURI + " delete query not found. Using defalut query \n" + deleteQueryText); + } + return deleteQueryText; + } + + private String getObjectMostSpecificType(String individualURI, VitroRequest vreq) { + String type = ""; + try { + ResultSet results = QueryUtils.getLanguageNeutralQueryResults(makeTypeQuery(individualURI), vreq); + while (results.hasNext()) { + QuerySolution solution = results.nextSolution(); + type = solution.get("type").toString(); + log.debug(type); + } + } catch (Exception e) { + log.error("Failed to get type for individual URI " + individualURI); + log.error(e, e); + } + return type; + } + + private byte[] getIndividualsToDelete(String targetIndividual, String deleteQuery,VitroRequest vreq) { + ByteArrayOutputStream out = new ByteArrayOutputStream(); + try { + Query queryForTypeSpecificDeleteQuery = QueryFactory.create(deleteQuery); + QuerySolutionMap initialBindings = new QuerySolutionMap(); + initialBindings.add("individualURI", ResourceFactory.createResource( targetIndividual )); + Model ontModel = vreq.getJenaOntModel(); + QueryExecution qexec = QueryExecutionFactory.create(queryForTypeSpecificDeleteQuery,ontModel,initialBindings ); + Model results = qexec.execDescribe(); + results.write(out,"N3"); + + } catch (Exception e) { + log.error("Query raised an error \n" + deleteQuery); + log.error(e, e); + } + return out.toByteArray(); + } + + private String makeTypeQuery(String objectURI) { + return TYPE_QUERY_START + objectURI + TYPE_QUERY_END; + } + + private void deleteIndividuals(byte[] toRemove, VitroRequest vreq) { + String removingString = new String(toRemove, StandardCharsets.UTF_8); + RDFService rdfService = vreq.getRDFService(); + ChangeSet cs = makeChangeSet(rdfService); + InputStream in = new ByteArrayInputStream(toRemove); + cs.addRemoval(in, RDFServiceUtils.getSerializationFormatFromJenaString("N3"), ModelNames.ABOX_ASSERTIONS); + try { + rdfService.changeSetUpdate(cs); + } catch (RDFServiceException e) { + log.error("Got error while removing\n" + removingString); + 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/controller/freemarker/UrlBuilder.java b/api/src/main/java/edu/cornell/mannlib/vitro/webapp/controller/freemarker/UrlBuilder.java index 8e4d4ae25..de4057d6e 100644 --- a/api/src/main/java/edu/cornell/mannlib/vitro/webapp/controller/freemarker/UrlBuilder.java +++ b/api/src/main/java/edu/cornell/mannlib/vitro/webapp/controller/freemarker/UrlBuilder.java @@ -37,6 +37,7 @@ public class UrlBuilder { LOGIN("/login"), LOGOUT("/logout"), OBJECT_PROPERTY_EDIT("/propertyEdit"), + CUSTOMSEARCH("/customsearch"), SEARCH("/search"), SITE_ADMIN("/siteAdmin"), TERMS_OF_USE("/termsOfUse"), diff --git a/api/src/main/java/edu/cornell/mannlib/vitro/webapp/dao/VitroVocabulary.java b/api/src/main/java/edu/cornell/mannlib/vitro/webapp/dao/VitroVocabulary.java index fdd9387c0..fba900cad 100644 --- a/api/src/main/java/edu/cornell/mannlib/vitro/webapp/dao/VitroVocabulary.java +++ b/api/src/main/java/edu/cornell/mannlib/vitro/webapp/dao/VitroVocabulary.java @@ -155,6 +155,7 @@ public class VitroVocabulary { public static final String USERACCOUNT_LAST_LOGIN_TIME = VITRO_AUTH + "lastLoginTime"; public static final String USERACCOUNT_STATUS = VITRO_AUTH + "status"; public static final String USERACCOUNT_PASSWORD_LINK_EXPIRES = VITRO_AUTH + "passwordLinkExpires"; + public static final String USERACCOUNT_EMAIL_KEY = VITRO_AUTH + "emailKey"; public static final String USERACCOUNT_PASSWORD_CHANGE_REQUIRED = VITRO_AUTH + "passwordChangeRequired"; public static final String USERACCOUNT_EXTERNAL_AUTH_ID = VITRO_AUTH + "externalAuthId"; public static final String USERACCOUNT_EXTERNAL_AUTH_ONLY = VITRO_AUTH + "externalAuthOnly"; diff --git a/api/src/main/java/edu/cornell/mannlib/vitro/webapp/dao/jena/JenaBaseDaoCon.java b/api/src/main/java/edu/cornell/mannlib/vitro/webapp/dao/jena/JenaBaseDaoCon.java index d618ac804..71fd6447a 100644 --- a/api/src/main/java/edu/cornell/mannlib/vitro/webapp/dao/jena/JenaBaseDaoCon.java +++ b/api/src/main/java/edu/cornell/mannlib/vitro/webapp/dao/jena/JenaBaseDaoCon.java @@ -121,6 +121,7 @@ public class JenaBaseDaoCon { protected DatatypeProperty USERACCOUNT_LAST_LOGIN_TIME = _constModel.createDatatypeProperty(VitroVocabulary.USERACCOUNT_LAST_LOGIN_TIME); protected DatatypeProperty USERACCOUNT_STATUS = _constModel.createDatatypeProperty(VitroVocabulary.USERACCOUNT_STATUS); protected DatatypeProperty USERACCOUNT_PASSWORD_LINK_EXPIRES = _constModel.createDatatypeProperty(VitroVocabulary.USERACCOUNT_PASSWORD_LINK_EXPIRES); + protected DatatypeProperty USERACCOUNT_EMAIL_KEY = _constModel.createDatatypeProperty(VitroVocabulary.USERACCOUNT_EMAIL_KEY); protected DatatypeProperty USERACCOUNT_PASSWORD_CHANGE_REQUIRED = _constModel.createDatatypeProperty(VitroVocabulary.USERACCOUNT_PASSWORD_CHANGE_REQUIRED); protected DatatypeProperty USERACCOUNT_EXTERNAL_AUTH_ID = _constModel.createDatatypeProperty(VitroVocabulary.USERACCOUNT_EXTERNAL_AUTH_ID); protected DatatypeProperty USERACCOUNT_EXTERNAL_AUTH_ONLY = _constModel.createDatatypeProperty(VitroVocabulary.USERACCOUNT_EXTERNAL_AUTH_ONLY); diff --git a/api/src/main/java/edu/cornell/mannlib/vitro/webapp/dao/jena/RDFServiceGraph.java b/api/src/main/java/edu/cornell/mannlib/vitro/webapp/dao/jena/RDFServiceGraph.java index 491858453..9b6642c4d 100644 --- a/api/src/main/java/edu/cornell/mannlib/vitro/webapp/dao/jena/RDFServiceGraph.java +++ b/api/src/main/java/edu/cornell/mannlib/vitro/webapp/dao/jena/RDFServiceGraph.java @@ -12,10 +12,10 @@ import java.util.function.Supplier; import org.apache.commons.lang3.StringUtils; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; - import org.apache.jena.graph.Capabilities; import org.apache.jena.graph.Graph; import org.apache.jena.graph.GraphEventManager; +import org.apache.jena.graph.GraphListener; import org.apache.jena.graph.GraphStatisticsHandler; import org.apache.jena.graph.Node; import org.apache.jena.graph.TransactionHandler; @@ -23,7 +23,6 @@ import org.apache.jena.graph.Triple; import org.apache.jena.graph.impl.GraphWithPerform; import org.apache.jena.graph.impl.SimpleEventManager; import org.apache.jena.query.QuerySolution; -import org.apache.jena.rdf.listeners.StatementListener; import org.apache.jena.rdf.model.Model; import org.apache.jena.rdf.model.ModelFactory; import org.apache.jena.rdf.model.StmtIterator; @@ -404,7 +403,18 @@ public class RDFServiceGraph implements GraphWithPerform { @Override public GraphEventManager getEventManager() { if (eventManager == null) { - eventManager = new SimpleEventManager(this); + eventManager = new SimpleEventManager() { + @Override + public void notifyEvent(Graph g, Object event) { + ChangeSet changeSet = rdfService.manufactureChangeSet(); + changeSet.addPreChangeEvent(event); + try { + rdfService.changeSetUpdate(changeSet); + } catch (RDFServiceException e) { + throw new RuntimeException(e); + } + } + }; } return eventManager; } @@ -590,21 +600,7 @@ public class RDFServiceGraph implements GraphWithPerform { } public static Model createRDFServiceModel(final RDFServiceGraph g) { - Model m = VitroModelFactory.createModelForGraph(g); - m.register(new StatementListener() { - @Override - public void notifyEvent(Model m, Object event) { - ChangeSet changeSet = g.getRDFService().manufactureChangeSet(); - changeSet.addPreChangeEvent(event); - try { - g.getRDFService().changeSetUpdate(changeSet); - } catch (RDFServiceException e) { - throw new RuntimeException(e); - } - } - - }); - return m; + return VitroModelFactory.createModelForGraph(g); } @Override diff --git a/api/src/main/java/edu/cornell/mannlib/vitro/webapp/dao/jena/UserAccountsDaoJena.java b/api/src/main/java/edu/cornell/mannlib/vitro/webapp/dao/jena/UserAccountsDaoJena.java index 73557fd5c..29c1d5a29 100644 --- a/api/src/main/java/edu/cornell/mannlib/vitro/webapp/dao/jena/UserAccountsDaoJena.java +++ b/api/src/main/java/edu/cornell/mannlib/vitro/webapp/dao/jena/UserAccountsDaoJena.java @@ -4,7 +4,6 @@ package edu.cornell.mannlib.vitro.webapp.dao.jena; import java.util.ArrayList; import java.util.Collection; -import java.util.Collections; import java.util.Comparator; import java.util.List; import java.util.Random; @@ -98,6 +97,8 @@ public class UserAccountsDaoJena extends JenaBaseDao implements UserAccountsDao u.setOldPassword(getPropertyStringValue(r, USERACCOUNT_OLD_PASSWORD)); u.setPasswordLinkExpires(getPropertyLongValue(r, USERACCOUNT_PASSWORD_LINK_EXPIRES)); + u.setEmailKey(getPropertyStringValue(r,USERACCOUNT_EMAIL_KEY)); + u.setPasswordChangeRequired(getPropertyBooleanValue(r, USERACCOUNT_PASSWORD_CHANGE_REQUIRED)); u.setExternalAuthOnly(getPropertyBooleanValue(r, @@ -240,6 +241,8 @@ public class UserAccountsDaoJena extends JenaBaseDao implements UserAccountsDao userAccount.getLoginCount(), model); addPropertyLongValue(res, USERACCOUNT_LAST_LOGIN_TIME, userAccount.getLastLoginTime(), model); + addPropertyStringValue(res, USERACCOUNT_EMAIL_KEY, + userAccount.getEmailKey(), model); if (userAccount.getStatus() != null) { addPropertyStringValue(res, USERACCOUNT_STATUS, userAccount .getStatus().toString(), model); @@ -306,6 +309,8 @@ public class UserAccountsDaoJena extends JenaBaseDao implements UserAccountsDao userAccount.getLoginCount(), model); updatePropertyLongValue(res, USERACCOUNT_LAST_LOGIN_TIME, userAccount.getLastLoginTime(), model); + updatePropertyStringValue(res, USERACCOUNT_EMAIL_KEY, + userAccount.getEmailKey(), model); if (userAccount.getStatus() == null) { updatePropertyStringValue(res, USERACCOUNT_STATUS, null, model); } else { diff --git a/api/src/main/java/edu/cornell/mannlib/vitro/webapp/edit/n3editing/VTwo/EditConfigurationUtils.java b/api/src/main/java/edu/cornell/mannlib/vitro/webapp/edit/n3editing/VTwo/EditConfigurationUtils.java index e72e172c1..da3f9be3e 100644 --- a/api/src/main/java/edu/cornell/mannlib/vitro/webapp/edit/n3editing/VTwo/EditConfigurationUtils.java +++ b/api/src/main/java/edu/cornell/mannlib/vitro/webapp/edit/n3editing/VTwo/EditConfigurationUtils.java @@ -61,6 +61,10 @@ public class EditConfigurationUtils { return vreq.getParameter("rangeUri"); } + public static String getTypeOfNew(VitroRequest vreq) { + return vreq.getParameter("typeOfNew"); + } + public static VClass getRangeVClass(VitroRequest vreq) { // This needs a WebappDaoFactory with no filtering/RDFService // funny business because it needs to be able to retrieve anonymous union 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 7b77786d4..c329543fe 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 @@ -28,7 +28,9 @@ public class DefaultDeleteGenerator extends BaseEditConfigurationGenerator imple private Integer dataHash = 0; private DataPropertyStatement dps = null; private String dataLiteral = null; - private String template = "confirmDeletePropertyForm.ftl"; + private String propertyTemplate = "confirmDeletePropertyForm.ftl"; + private String individualTemplate = "confirmDeleteIndividualForm.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,12 +45,32 @@ public class DefaultDeleteGenerator extends BaseEditConfigurationGenerator imple if(editConfiguration == null) { editConfiguration = setupEditConfiguration(vreq, session); } - editConfiguration.setTemplate(template); //prepare update? prepare(vreq, editConfiguration); + if (editConfiguration.getPredicateUri() == null && editConfiguration.getSubjectUri() == null) { + editConfiguration.setTemplate(individualTemplate); + addDeleteParams(vreq, editConfiguration); + }else { + editConfiguration.setTemplate(propertyTemplate); + } 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); 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 96d6556b8..20b894878 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 @@ -11,6 +11,7 @@ import javax.servlet.annotation.WebServlet; import javax.servlet.http.HttpServletResponse; import javax.servlet.http.HttpSession; +import org.apache.commons.lang3.StringUtils; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import org.apache.jena.ontology.OntModel; @@ -67,10 +68,20 @@ public class EditRequestDispatchController extends FreemarkerHttpServlet { //TODO: Create this generator final String RDFS_LABEL_FORM = "edu.cornell.mannlib.vitro.webapp.edit.n3editing.configuration.generators.RDFSLabelGenerator"; final String DEFAULT_DELETE_FORM = "edu.cornell.mannlib.vitro.webapp.edit.n3editing.configuration.generators.DefaultDeleteGenerator"; - - @Override - protected AuthorizationRequest requiredActions(VitroRequest vreq) { - //Check if this statement can be edited here and return unauthorized if not + final String MANAGE_MENUS_FORM = "edu.cornell.mannlib.vitro.webapp.edit.n3editing.configuration.generators.ManagePageGenerator"; + + @Override + protected AuthorizationRequest requiredActions(VitroRequest vreq) { + // If request is for new individual, return simple do back end editing action permission + if (StringUtils.isNotEmpty(EditConfigurationUtils.getTypeOfNew(vreq))) { + return SimplePermission.DO_BACK_END_EDITING.ACTION; + } 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); String objectUri = EditConfigurationUtils.getObjectUri(vreq); @@ -98,6 +109,16 @@ public class EditRequestDispatchController extends FreemarkerHttpServlet { 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) { @@ -355,7 +376,7 @@ public class EditRequestDispatchController extends FreemarkerHttpServlet { 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/freemarker/config/FreemarkerConfigurationImpl.java b/api/src/main/java/edu/cornell/mannlib/vitro/webapp/freemarker/config/FreemarkerConfigurationImpl.java index 10e2d9926..85d6282cf 100644 --- a/api/src/main/java/edu/cornell/mannlib/vitro/webapp/freemarker/config/FreemarkerConfigurationImpl.java +++ b/api/src/main/java/edu/cornell/mannlib/vitro/webapp/freemarker/config/FreemarkerConfigurationImpl.java @@ -304,6 +304,7 @@ public class FreemarkerConfigurationImpl extends Configuration { urls.put("home", UrlBuilder.getHomeUrl()); urls.put("about", UrlBuilder.getUrl(Route.ABOUT)); urls.put("search", UrlBuilder.getUrl(Route.SEARCH)); + urls.put("customsearch", UrlBuilder.getUrl(Route.CUSTOMSEARCH)); urls.put("termsOfUse", UrlBuilder.getUrl(Route.TERMS_OF_USE)); urls.put("login", UrlBuilder.getLoginUrl()); urls.put("logout", UrlBuilder.getLogoutUrl()); diff --git a/api/src/main/java/edu/cornell/mannlib/vitro/webapp/search/controller/CustomSearchController.java b/api/src/main/java/edu/cornell/mannlib/vitro/webapp/search/controller/CustomSearchController.java new file mode 100644 index 000000000..f58418f86 --- /dev/null +++ b/api/src/main/java/edu/cornell/mannlib/vitro/webapp/search/controller/CustomSearchController.java @@ -0,0 +1,766 @@ +/* $This file is distributed under the terms of the license in LICENSE$ */ + +package edu.cornell.mannlib.vitro.webapp.search.controller; + +import java.io.IOException; +import java.sql.SQLException; +import java.util.ArrayList; +import java.util.Base64; +import java.util.Collection; +import java.util.Collections; +import java.util.Comparator; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; + +import javax.servlet.ServletException; +import javax.servlet.annotation.WebServlet; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +import org.apache.commons.lang3.StringUtils; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; + +import edu.cornell.mannlib.vitro.webapp.application.ApplicationUtils; +import edu.cornell.mannlib.vitro.webapp.beans.ApplicationBean; +import edu.cornell.mannlib.vitro.webapp.beans.Individual; +import edu.cornell.mannlib.vitro.webapp.beans.VClass; +import edu.cornell.mannlib.vitro.webapp.beans.VClassGroup; +import edu.cornell.mannlib.vitro.webapp.controller.VitroRequest; +import edu.cornell.mannlib.vitro.webapp.controller.freemarker.FreemarkerHttpServlet; +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.responsevalues.ExceptionResponseValues; +import edu.cornell.mannlib.vitro.webapp.controller.freemarker.responsevalues.ResponseValues; +import edu.cornell.mannlib.vitro.webapp.controller.freemarker.responsevalues.TemplateResponseValues; +import edu.cornell.mannlib.vitro.webapp.dao.IndividualDao; +import edu.cornell.mannlib.vitro.webapp.dao.VClassDao; +import edu.cornell.mannlib.vitro.webapp.dao.VClassGroupDao; +import edu.cornell.mannlib.vitro.webapp.dao.VClassGroupsForRequest; +import edu.cornell.mannlib.vitro.webapp.dao.VitroVocabulary; +import edu.cornell.mannlib.vitro.webapp.dao.jena.VClassGroupCache; +import edu.cornell.mannlib.vitro.webapp.i18n.I18n; +import edu.cornell.mannlib.vitro.webapp.modules.searchEngine.SearchEngine; +import edu.cornell.mannlib.vitro.webapp.modules.searchEngine.SearchFacetField; +import edu.cornell.mannlib.vitro.webapp.modules.searchEngine.SearchFacetField.Count; +import edu.cornell.mannlib.vitro.webapp.modules.searchEngine.SearchQuery; +import edu.cornell.mannlib.vitro.webapp.modules.searchEngine.SearchResponse; +import edu.cornell.mannlib.vitro.webapp.modules.searchEngine.SearchResultDocument; +import edu.cornell.mannlib.vitro.webapp.modules.searchEngine.SearchResultDocumentList; +import edu.cornell.mannlib.vitro.webapp.search.VitroSearchTermNames; +import edu.cornell.mannlib.vitro.webapp.web.templatemodels.LinkTemplateModel; +import edu.cornell.mannlib.vitro.webapp.web.templatemodels.searchresult.IndividualSearchResult; +import edu.ucsf.vitro.opensocial.OpenSocialManager; + +/** + * Paged search controller that uses the search engine + */ + +@WebServlet(name = "CustomSearchController", urlPatterns = {"/customsearch","/customsearch.jsp","/customfedsearch","/customsearchcontroller"} ) +public class CustomSearchController extends FreemarkerHttpServlet { + + private static final long serialVersionUID = 1L; + private static final Log log = LogFactory.getLog(CustomSearchController.class); + + protected static final int DEFAULT_HITS_PER_PAGE = 25; + protected static final int DEFAULT_MAX_HIT_COUNT = 1000; + + private static final String PARAM_XML_REQUEST = "xml"; + private static final String PARAM_CSV_REQUEST = "csv"; + private static final String PARAM_START_INDEX = "startIndex"; + private static final String PARAM_HITS_PER_PAGE = "hitsPerPage"; + private static final String PARAM_CLASSGROUP = "classgroup"; + private static final String PARAM_RDFTYPE = "type"; + private static final String PARAM_QUERY_TEXT = "querytext"; + + protected static final Map> templateTable; + + protected enum Format { + HTML, XML, CSV; + } + + protected enum Result { + PAGED, ERROR, BAD_QUERY + } + + static{ + templateTable = setupTemplateTable(); + } + + /** + * Overriding doGet from FreemarkerHttpController to do a page template (as + * opposed to body template) style output for XML requests. + * + * This follows the pattern in AutocompleteController.java. + */ + @Override + public void doGet(HttpServletRequest request, HttpServletResponse response) + throws IOException, ServletException { + VitroRequest vreq = new VitroRequest(request); + boolean wasXmlRequested = isRequestedFormatXml(vreq); + boolean wasCSVRequested = isRequestedFormatCSV(vreq); + if( !wasXmlRequested && !wasCSVRequested){ + super.doGet(vreq,response); + }else if (wasXmlRequested){ + try { + ResponseValues rvalues = processRequest(vreq); + + response.setCharacterEncoding("UTF-8"); + response.setContentType("text/xml;charset=UTF-8"); + response.setHeader("Content-Disposition", "attachment; filename=search.xml"); + writeTemplate(rvalues.getTemplateName(), rvalues.getMap(), request, response); + } catch (Exception e) { + log.error(e, e); + } + }else if (wasCSVRequested){ + try { + ResponseValues rvalues = processRequest(vreq); + + response.setCharacterEncoding("UTF-8"); + response.setContentType("text/csv;charset=UTF-8"); + response.setHeader("Content-Disposition", "attachment; filename=search.csv"); + writeTemplate(rvalues.getTemplateName(), rvalues.getMap(), request, response); + } catch (Exception e) { + log.error(e, e); + } + } + } + + @Override + public void doPost(HttpServletRequest request, HttpServletResponse response) + throws IOException, ServletException { + VitroRequest vreq = new VitroRequest(request); + boolean wasXmlRequested = isRequestedFormatXml(vreq); + boolean wasCSVRequested = isRequestedFormatCSV(vreq); + if( !wasXmlRequested && !wasCSVRequested){ + super.doGet(vreq,response); + }else if (wasXmlRequested){ + try { + ResponseValues rvalues = processRequest(vreq); + + response.setCharacterEncoding("UTF-8"); + response.setContentType("text/xml;charset=UTF-8"); + response.setHeader("Content-Disposition", "attachment; filename=search.xml"); + writeTemplate(rvalues.getTemplateName(), rvalues.getMap(), request, response); + } catch (Exception e) { + log.error(e, e); + } + }else if (wasCSVRequested){ + try { + ResponseValues rvalues = processRequest(vreq); + + response.setCharacterEncoding("UTF-8"); + response.setContentType("text/csv;charset=UTF-8"); + response.setHeader("Content-Disposition", "attachment; filename=search.csv"); + writeTemplate(rvalues.getTemplateName(), rvalues.getMap(), request, response); + } catch (Exception e) { + log.error(e, e); + } + } + } + + @Override + protected ResponseValues processRequest(VitroRequest vreq) { + + //There may be other non-html formats in the future + Format format = getFormat(vreq); + boolean wasXmlRequested = Format.XML == format; + boolean wasCSVRequested = Format.CSV == format; + log.debug("Requested format was " + (wasXmlRequested ? "xml" : "html")); + boolean wasHtmlRequested = ! (wasXmlRequested || wasCSVRequested); + + try { + + //make sure an IndividualDao is available + if( vreq.getWebappDaoFactory() == null + || vreq.getWebappDaoFactory().getIndividualDao() == null ){ + log.error("Could not get webappDaoFactory or IndividualDao"); + throw new Exception("Could not access model."); + } + IndividualDao iDao = vreq.getWebappDaoFactory().getIndividualDao(); + VClassGroupDao grpDao = vreq.getWebappDaoFactory().getVClassGroupDao(); + VClassDao vclassDao = vreq.getWebappDaoFactory().getVClassDao(); + + ApplicationBean appBean = vreq.getAppBean(); + + log.debug("IndividualDao is " + iDao.toString() + " Public classes in the classgroup are " + grpDao.getPublicGroupsWithVClasses().toString()); + log.debug("VClassDao is "+ vclassDao.toString() ); + + int startIndex = getStartIndex(vreq); + int hitsPerPage = getHitsPerPage( vreq ); + String queryBuilderRules = getQueryBuilderRules(vreq); + + + String queryText = vreq.getParameter(PARAM_QUERY_TEXT); + log.debug("Query text is \""+ queryText + "\""); + + + String badQueryMsg = badQueryText( queryText, vreq ); + if( badQueryMsg != null ){ + return doFailedSearch(badQueryMsg, queryText, format, vreq); + } + + SearchQuery query = getQuery(queryText, hitsPerPage, startIndex, vreq); + SearchEngine search = ApplicationUtils.instance().getSearchEngine(); + SearchResponse response = null; + + try { + response = search.query(query); + } catch (Exception ex) { + String msg = makeBadSearchMessage(queryText, ex.getMessage(), vreq); + log.error("could not run search query",ex); + return doFailedSearch(msg, queryText, format, vreq); + } + + if (response == null) { + log.error("Search response was null"); + return doFailedSearch(I18n.text(vreq, "error_in_search_request"), queryText, format, vreq); + } + + SearchResultDocumentList docs = response.getResults(); + if (docs == null) { + log.error("Document list for a search was null"); + return doFailedSearch(I18n.text(vreq, "error_in_search_request"), queryText,format, vreq); + } + + long hitCount = docs.getNumFound(); + log.debug("Number of hits = " + hitCount); + if ( hitCount < 1 ) { + return doNoHits(queryText,format, vreq); + } + + List individuals = new ArrayList(docs.size()); + for (SearchResultDocument doc : docs) { + try { + String uri = doc.getStringValue(VitroSearchTermNames.URI); + Individual ind = iDao.getIndividualByURI(uri); + if (ind != null) { + ind.setSearchSnippet(getSnippet(doc, response)); + individuals.add(ind); + } + } catch (Exception e) { + log.error("Problem getting usable individuals from search hits. ", e); + } + } + + ParamMap pagingLinkParams = new ParamMap(); + pagingLinkParams.put(PARAM_QUERY_TEXT, queryText); + pagingLinkParams.put(PARAM_HITS_PER_PAGE, String.valueOf(hitsPerPage)); + + if( wasXmlRequested ){ + pagingLinkParams.put(PARAM_XML_REQUEST,"1"); + } + + /* Compile the data for the templates */ + + Map body = new HashMap(); + + String classGroupParam = vreq.getParameter(PARAM_CLASSGROUP); + log.debug("ClassGroupParam is \""+ classGroupParam + "\""); + boolean classGroupFilterRequested = false; + if (!StringUtils.isEmpty(classGroupParam)) { + VClassGroup grp = grpDao.getGroupByURI(classGroupParam); + classGroupFilterRequested = true; + if (grp != null && grp.getPublicName() != null) + body.put("classGroupName", grp.getPublicName()); + } + + String typeParam = vreq.getParameter(PARAM_RDFTYPE); + boolean typeFilterRequested = false; + if (!StringUtils.isEmpty(typeParam)) { + VClass type = vclassDao.getVClassByURI(typeParam); + typeFilterRequested = true; + if (type != null && type.getName() != null) + body.put("typeName", type.getName()); + } + + /* Add ClassGroup and type refinement links to body */ + if( wasHtmlRequested ){ + if ( !classGroupFilterRequested && !typeFilterRequested ) { + // Search request includes no ClassGroup and no type, so add ClassGroup search refinement links. + body.put("classGroupLinks", getClassGroupsLinks(vreq, grpDao, docs, response, queryText)); + } else if ( classGroupFilterRequested && !typeFilterRequested ) { + // Search request is for a ClassGroup, so add rdf:type search refinement links + // but try to filter out classes that are subclasses + body.put("classLinks", getVClassLinks(vclassDao, docs, response, queryText)); + pagingLinkParams.put(PARAM_CLASSGROUP, classGroupParam); + + } else { + //search request is for a class so there are no more refinements + pagingLinkParams.put(PARAM_RDFTYPE, typeParam); + } + } + + body.put("individuals", IndividualSearchResult + .getIndividualTemplateModels(individuals, vreq)); + + body.put("querytext", queryText); + body.put("title", new StringBuilder().append(appBean.getApplicationName()).append(" - "). + append(I18n.text(vreq, "search_results_for")).append(" '").append(queryText).append("'").toString()); + + body.put("hitCount", hitCount); + body.put("startIndex", startIndex); + body.put(PARAM_HITS_PER_PAGE, hitsPerPage); + + body.put("pagingLinks", + getPagingLinks(startIndex, hitsPerPage, hitCount, + vreq.getServletPath(), + pagingLinkParams, vreq)); + + if (startIndex != 0) { + body.put("prevPage", getPreviousPageLink(startIndex, + hitsPerPage, vreq.getServletPath(), pagingLinkParams)); + } + if (startIndex < (hitCount - hitsPerPage)) { + body.put("nextPage", getNextPageLink(startIndex, hitsPerPage, + vreq.getServletPath(), pagingLinkParams)); + } + if (queryBuilderRules != null) { + body.put("queryBuilderRules", queryBuilderRules); + } + body.put(PARAM_HITS_PER_PAGE, hitsPerPage); + + // VIVO OpenSocial Extension by UCSF + try { + OpenSocialManager openSocialManager = new OpenSocialManager(vreq, "search"); + // put list of people found onto pubsub channel + // only turn this on for a people only search + if ("http://vivoweb.org/ontology#vitroClassGrouppeople".equals(vreq.getParameter(PARAM_CLASSGROUP))) { + List ids = OpenSocialManager.getOpenSocialId(individuals); + openSocialManager.setPubsubData(OpenSocialManager.JSON_PERSONID_CHANNEL, + OpenSocialManager.buildJSONPersonIds(ids, "" + ids.size() + " people found")); + } + // TODO put this in a better place to guarantee that it gets called at the proper time! + openSocialManager.removePubsubGadgetsWithoutData(); + body.put("openSocial", openSocialManager); + if (openSocialManager.isVisible()) { + body.put("bodyOnload", "my.init();"); + } + } catch (IOException e) { + log.error("IOException in doTemplate()", e); + } catch (SQLException e) { + log.error("SQLException in doTemplate()", e); + } + + String template = templateTable.get(format).get(Result.PAGED); + + + + return new TemplateResponseValues(template, body); + } catch (Throwable e) { + return doSearchError(e,format); + } + } + + private String getQueryBuilderRules(VitroRequest vreq) { + String rules = null; + try { + rules = vreq.getParameter("queryBuilderRules"); + } catch (Throwable e) { + log.error(e); + } + return rules; + } + + private int getHitsPerPage(VitroRequest vreq) { + int hitsPerPage = DEFAULT_HITS_PER_PAGE; + try{ + hitsPerPage = Integer.parseInt(vreq.getParameter(PARAM_HITS_PER_PAGE)); + } catch (Throwable e) { + hitsPerPage = DEFAULT_HITS_PER_PAGE; + } + log.debug("hitsPerPage is " + hitsPerPage); + return hitsPerPage; + } + + private int getStartIndex(VitroRequest vreq) { + int startIndex = 0; + try{ + startIndex = Integer.parseInt(vreq.getParameter(PARAM_START_INDEX)); + }catch (Throwable e) { + startIndex = 0; + } + log.debug("startIndex is " + startIndex); + return startIndex; + } + + private String badQueryText(String qtxt, VitroRequest vreq) { + if( qtxt == null || "".equals( qtxt.trim() ) ) + return I18n.text(vreq, "enter_search_term"); + + if( qtxt.equals("*:*") ) + return I18n.text(vreq, "invalid_search_term") ; + + return null; + } + + /** + * Get the class groups represented for the individuals in the documents. + */ + private List getClassGroupsLinks(VitroRequest vreq, VClassGroupDao grpDao, SearchResultDocumentList docs, SearchResponse rsp, String qtxt) { + Map cgURItoCount = new HashMap(); + + List classgroups = new ArrayList( ); + List ffs = rsp.getFacetFields(); + for(SearchFacetField ff : ffs){ + if(VitroSearchTermNames.CLASSGROUP_URI.equals(ff.getName())){ + List counts = ff.getValues(); + for( Count ct: counts){ + VClassGroup vcg = grpDao.getGroupByURI( ct.getName() ); + if( vcg == null ){ + log.debug("could not get classgroup for URI " + ct.getName()); + }else{ + classgroups.add(vcg); + cgURItoCount.put(vcg.getURI(), ct.getCount()); + } + } + } + } + + grpDao.sortGroupList(classgroups); + + VClassGroupsForRequest vcgfr = VClassGroupCache.getVClassGroups(vreq); + List classGroupLinks = new ArrayList(classgroups.size()); + for (VClassGroup vcg : classgroups) { + String groupURI = vcg.getURI(); + VClassGroup localizedVcg = vcgfr.getGroup(groupURI); + long count = cgURItoCount.get( groupURI ); + if (localizedVcg.getPublicName() != null && count > 0 ) { + classGroupLinks.add(new VClassGroupSearchLink(qtxt, localizedVcg, count)); + } + } + return classGroupLinks; + } + + private List getVClassLinks(VClassDao vclassDao, SearchResultDocumentList docs, SearchResponse rsp, String qtxt){ + HashSet typesInHits = getVClassUrisForHits(docs); + List classes = new ArrayList(typesInHits.size()); + Map typeURItoCount = new HashMap(); + + List ffs = rsp.getFacetFields(); + for(SearchFacetField ff : ffs){ + if(VitroSearchTermNames.RDFTYPE.equals(ff.getName())){ + List counts = ff.getValues(); + for( Count ct: counts){ + String typeUri = ct.getName(); + long count = ct.getCount(); + try{ + if( VitroVocabulary.OWL_THING.equals(typeUri) || + count == 0 ) + continue; + VClass type = vclassDao.getVClassByURI(typeUri); + if( type != null && + ! type.isAnonymous() && + type.getName() != null && !"".equals(type.getName()) && + type.getGroupURI() != null ){ //don't display classes that aren't in classgroups + typeURItoCount.put(typeUri,count); + classes.add(type); + } + }catch(Exception ex){ + if( log.isDebugEnabled() ) + log.debug("could not add type " + typeUri, ex); + } + } + } + } + + + classes.sort(new Comparator() { + public int compare(VClass o1, VClass o2) { + return o1.compareTo(o2); + } + }); + + List vClassLinks = new ArrayList(classes.size()); + for (VClass vc : classes) { + long count = typeURItoCount.get(vc.getURI()); + vClassLinks.add(new VClassSearchLink(qtxt, vc, count )); + } + + return vClassLinks; + } + + private HashSet getVClassUrisForHits(SearchResultDocumentList docs){ + HashSet typesInHits = new HashSet(); + for (SearchResultDocument doc : docs) { + try { + Collection types = doc.getFieldValues(VitroSearchTermNames.RDFTYPE); + if (types != null) { + for (Object o : types) { + String typeUri = o.toString(); + typesInHits.add(typeUri); + } + } + } catch (Exception e) { + log.error("problems getting rdf:type for search hits",e); + } + } + return typesInHits; + } + + private String getSnippet(SearchResultDocument doc, SearchResponse response) { + String docId = doc.getStringValue(VitroSearchTermNames.DOCID); + StringBuilder text = new StringBuilder(); + Map>> highlights = response.getHighlighting(); + if (highlights != null && highlights.get(docId) != null) { + List snippets = highlights.get(docId).get(VitroSearchTermNames.ALLTEXT); + if (snippets != null && snippets.size() > 0) { + text.append("... ").append(snippets.get(0)).append(" ..."); + } + } + return text.toString(); + } + + private SearchQuery getQuery(String queryText, int hitsPerPage, int startIndex, VitroRequest vreq) { + // Lowercase the search term to support wildcard searches: The search engine applies no text + // processing to a wildcard search term. + SearchQuery query = ApplicationUtils.instance().getSearchEngine().createQuery(queryText); + + query.setStart( startIndex ) + .setRows(hitsPerPage); + + // ClassGroup filtering param + String classgroupParam = vreq.getParameter(PARAM_CLASSGROUP); + + // rdf:type filtering param + String typeParam = vreq.getParameter(PARAM_RDFTYPE); + + if ( ! StringUtils.isBlank(classgroupParam) ) { + // ClassGroup filtering + log.debug("Firing classgroup query "); + log.debug("request.getParameter(classgroup) is "+ classgroupParam); + query.addFilterQuery(VitroSearchTermNames.CLASSGROUP_URI + ":\"" + classgroupParam + "\""); + + //with ClassGroup filtering we want type facets + query.addFacetFields(VitroSearchTermNames.RDFTYPE).setFacetLimit(-1); + + }else if ( ! StringUtils.isBlank(typeParam) ) { + // rdf:type filtering + log.debug("Firing type query "); + log.debug("request.getParameter(type) is "+ typeParam); + query.addFilterQuery(VitroSearchTermNames.RDFTYPE + ":\"" + typeParam + "\""); + //with type filtering we don't have facets. + }else{ + //When no filtering is set, we want ClassGroup facets + query.addFacetFields(VitroSearchTermNames.CLASSGROUP_URI).setFacetLimit(-1); + } + + log.debug("Query = " + query.toString()); + return query; + } + + public static class VClassGroupSearchLink extends LinkTemplateModel { + long count = 0; + VClassGroupSearchLink(String querytext, VClassGroup classgroup, long count) { + super(classgroup.getPublicName(), "/customsearch", PARAM_QUERY_TEXT, querytext, PARAM_CLASSGROUP, classgroup.getURI()); + this.count = count; + } + + public String getCount() { return Long.toString(count); } + } + + public static class VClassSearchLink extends LinkTemplateModel { + long count = 0; + VClassSearchLink(String querytext, VClass type, long count) { + super(type.getName(), "/customsearch", PARAM_QUERY_TEXT, querytext, PARAM_RDFTYPE, type.getURI()); + this.count = count; + } + + public String getCount() { return Long.toString(count); } + } + + protected static List getPagingLinks(int startIndex, int hitsPerPage, long hitCount, String baseUrl, ParamMap params, VitroRequest vreq) { + + List pagingLinks = new ArrayList(); + + // No paging links if only one page of results + if (hitCount <= hitsPerPage) { + return pagingLinks; + } + + int maxHitCount = DEFAULT_MAX_HIT_COUNT ; + if( startIndex >= DEFAULT_MAX_HIT_COUNT - hitsPerPage ) + maxHitCount = startIndex + DEFAULT_MAX_HIT_COUNT ; + + for (int i = 0; i < hitCount; i += hitsPerPage) { + params.put(PARAM_START_INDEX, String.valueOf(i)); + if ( i < maxHitCount - hitsPerPage) { + int pageNumber = i/hitsPerPage + 1; + boolean iIsCurrentPage = (i >= startIndex && i < (startIndex + hitsPerPage)); + if ( iIsCurrentPage ) { + pagingLinks.add(new PagingLink(pageNumber)); + } else { + pagingLinks.add(new PagingLink(pageNumber, baseUrl, params)); + } + } else { + pagingLinks.add(new PagingLink(I18n.text(vreq, "paging_link_more"), baseUrl, params)); + break; + } + } + + return pagingLinks; + } + + private String getPreviousPageLink(int startIndex, int hitsPerPage, String baseUrl, ParamMap params) { + params.put(PARAM_START_INDEX, String.valueOf(startIndex-hitsPerPage)); + return UrlBuilder.getUrl(baseUrl, params); + } + + private String getNextPageLink(int startIndex, int hitsPerPage, String baseUrl, ParamMap params) { + params.put(PARAM_START_INDEX, String.valueOf(startIndex+hitsPerPage)); + return UrlBuilder.getUrl(baseUrl, params); + } + + protected static class PagingLink extends LinkTemplateModel { + + PagingLink(int pageNumber, String baseUrl, ParamMap params) { + super(String.valueOf(pageNumber), baseUrl, params); + } + + // Constructor for current page item: not a link, so no url value. + PagingLink(int pageNumber) { + setText(String.valueOf(pageNumber)); + } + + // Constructor for "more..." item + PagingLink(String text, String baseUrl, ParamMap params) { + super(text, baseUrl, params); + } + } + + private ExceptionResponseValues doSearchError(Throwable e, Format f) { + Map body = new HashMap(); + body.put("message", "Search failed: " + e.getMessage()); + return new ExceptionResponseValues(getTemplate(f,Result.ERROR), body, e); + } + + private TemplateResponseValues doFailedSearch(String message, String querytext, Format f, VitroRequest vreq) { + Map body = new HashMap(); + body.put("title", I18n.text(vreq, "search_for", querytext)); + if ( StringUtils.isEmpty(message) ) { + message = I18n.text(vreq, "search_failed"); + } + body.put("message", message); + return new TemplateResponseValues(getTemplate(f,Result.ERROR), body); + } + + private TemplateResponseValues doNoHits(String querytext, Format f, VitroRequest vreq) { + Map body = new HashMap(); + body.put("title", I18n.text(vreq, "search_for", querytext)); + body.put("message", I18n.text(vreq, "no_matching_results")); + return new TemplateResponseValues(getTemplate(f,Result.ERROR), body); + } + + /** + * Makes a message to display to user for a bad search term. + */ + private String makeBadSearchMessage(String querytext, String exceptionMsg, VitroRequest vreq){ + String rv = ""; + try{ + //try to get the column in the search term that is causing the problems + int coli = exceptionMsg.indexOf("column"); + if( coli == -1) return ""; + int numi = exceptionMsg.indexOf(".", coli+7); + if( numi == -1 ) return ""; + String part = exceptionMsg.substring(coli+7,numi ); + int i = Integer.parseInt(part) - 1; + + // figure out where to cut preview and post-view + int errorWindow = 5; + int pre = i - errorWindow; + if (pre < 0) + pre = 0; + int post = i + errorWindow; + if (post > querytext.length()) + post = querytext.length(); + // log.warn("pre: " + pre + " post: " + post + " term len: + // " + term.length()); + + // get part of the search term before the error and after + String before = querytext.substring(pre, i); + String after = ""; + if (post > i) + after = querytext.substring(i + 1, post); + + rv = I18n.text(vreq, "search_term_error_near") + + " " + + before + "" + querytext.charAt(i) + + "" + after + ""; + } catch (Throwable ex) { + return ""; + } + return rv; + } + + public static final int MAX_QUERY_LENGTH = 500; + + protected boolean isRequestedFormatXml(VitroRequest req){ + if( req != null ){ + String param = req.getParameter(PARAM_XML_REQUEST); + return param != null && "1".equals(param); + }else{ + return false; + } + } + + protected boolean isRequestedFormatCSV(VitroRequest req){ + if( req != null ){ + String param = req.getParameter(PARAM_CSV_REQUEST); + return param != null && "1".equals(param); + }else{ + return false; + } + } + + protected Format getFormat(VitroRequest req){ + if( req != null && req.getParameter("xml") != null && "1".equals(req.getParameter("xml"))) + return Format.XML; + else if ( req != null && req.getParameter("csv") != null && "1".equals(req.getParameter("csv"))) + return Format.CSV; + else + return Format.HTML; + } + + protected static String getTemplate(Format format, Result result){ + if( format != null && result != null) + return templateTable.get(format).get(result); + else{ + log.error("getTemplate() must not have a null format or result."); + return templateTable.get(Format.HTML).get(Result.ERROR); + } + } + + protected static Map> setupTemplateTable(){ + Map> table = new HashMap<>(); + + HashMap resultsToTemplates = new HashMap(); + + // set up HTML format + resultsToTemplates.put(Result.PAGED, "search-pagedResults.ftl"); + resultsToTemplates.put(Result.ERROR, "search-error.ftl"); + // resultsToTemplates.put(Result.BAD_QUERY, "search-badQuery.ftl"); + table.put(Format.HTML, Collections.unmodifiableMap(resultsToTemplates)); + + // set up XML format + resultsToTemplates = new HashMap(); + resultsToTemplates.put(Result.PAGED, "search-xmlResults.ftl"); + resultsToTemplates.put(Result.ERROR, "search-xmlError.ftl"); + + // resultsToTemplates.put(Result.BAD_QUERY, "search-xmlBadQuery.ftl"); + table.put(Format.XML, Collections.unmodifiableMap(resultsToTemplates)); + + + // set up CSV format + resultsToTemplates = new HashMap(); + resultsToTemplates.put(Result.PAGED, "search-csvResults.ftl"); + resultsToTemplates.put(Result.ERROR, "search-csvError.ftl"); + + // resultsToTemplates.put(Result.BAD_QUERY, "search-xmlBadQuery.ftl"); + table.put(Format.CSV, Collections.unmodifiableMap(resultsToTemplates)); + + + return Collections.unmodifiableMap(table); + } +} 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 7f88848ec..05a9c676f 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 @@ -705,6 +705,10 @@ public class EditConfigurationTemplateModel extends BaseTemplateModel { 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() { 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 8c25395ff..efaa8396f 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.RequestedAct 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.beans.VClass; 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 @@ import edu.cornell.mannlib.vitro.webapp.web.templatemodels.BaseTemplateModel; 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 abstract class BaseIndividualTemplateModel extends BaseTemplateModel { 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/api/src/main/resources/edu/cornell/mannlib/vitro/webapp/web/antisamy-vitro-1.4.4.xml b/api/src/main/resources/edu/cornell/mannlib/vitro/webapp/web/antisamy-vitro-1.4.4.xml index 80d38bf37..546c08b37 100644 --- a/api/src/main/resources/edu/cornell/mannlib/vitro/webapp/web/antisamy-vitro-1.4.4.xml +++ b/api/src/main/resources/edu/cornell/mannlib/vitro/webapp/web/antisamy-vitro-1.4.4.xml @@ -106,8 +106,8 @@ http://www.w3.org/TR/html401/struct/global.html - - + + @@ -149,6 +149,14 @@ http://www.w3.org/TR/html401/struct/global.html + + + + + + + + @@ -549,12 +557,24 @@ http://www.w3.org/TR/html401/struct/global.html - - - - - - + + + + + + + + + + + + + + + + + + diff --git a/api/src/test/java/edu/cornell/mannlib/vitro/webapp/dao/jena/RDFServiceGraphTest.java b/api/src/test/java/edu/cornell/mannlib/vitro/webapp/dao/jena/RDFServiceGraphTest.java new file mode 100644 index 000000000..e5c8a65fa --- /dev/null +++ b/api/src/test/java/edu/cornell/mannlib/vitro/webapp/dao/jena/RDFServiceGraphTest.java @@ -0,0 +1,62 @@ +package edu.cornell.mannlib.vitro.webapp.dao.jena; + +import static org.junit.Assert.assertEquals; + +import org.apache.jena.rdf.model.Model; +import org.apache.jena.rdf.model.ModelFactory; +import org.junit.Test; + +import edu.cornell.mannlib.vitro.testing.AbstractTestClass; +import edu.cornell.mannlib.vitro.webapp.rdfservice.ChangeListener; +import edu.cornell.mannlib.vitro.webapp.rdfservice.ModelChange; +import edu.cornell.mannlib.vitro.webapp.rdfservice.RDFService; +import edu.cornell.mannlib.vitro.webapp.rdfservice.RDFServiceException; +import edu.cornell.mannlib.vitro.webapp.rdfservice.impl.jena.model.RDFServiceModel; + + +public class RDFServiceGraphTest extends AbstractTestClass { + + @Test + /** + * Test that creating a new model with the same underlying RDFServiceGraph + * does not result in a new listener registered on that graph. No matter + * how many models have been created using a given RDFServiceGraph, an event + * sent to the last-created model should be heard only once by the + * RDFService. + * @throws RDFServiceException + */ + public void testEventListening() throws RDFServiceException { + Model m = ModelFactory.createDefaultModel(); + RDFService rdfService = new RDFServiceModel(m); + EventsCounter counter = new EventsCounter(); + rdfService.registerListener(counter); + RDFServiceGraph g = new RDFServiceGraph(rdfService); + Model model = null; + for (int i = 0; i < 100; i++) { + model = RDFServiceGraph.createRDFServiceModel(g); + } + model.notifyEvent("event"); + assertEquals(1, counter.getCount()); + } + + private class EventsCounter implements ChangeListener { + + private int count = 0; + + public int getCount() { + return count; + } + + @Override + public void notifyModelChange(ModelChange modelChange) { + // TODO Auto-generated method stub + } + + @Override + public void notifyEvent(String graphURI, Object event) { + count++; + } + + } + +} diff --git a/home/src/main/resources/rdf/display/firsttime/application.owl b/home/src/main/resources/rdf/display/firsttime/application.owl index 02dbd5736..3a48d3ef5 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 d5fcf9ec2..07478b771 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/i18n/all.properties b/webapp/src/main/webapp/i18n/all.properties index 1909545ad..5f7be8861 100644 --- a/webapp/src/main/webapp/i18n/all.properties +++ b/webapp/src/main/webapp/i18n/all.properties @@ -720,6 +720,7 @@ there_are_no_entries_for_selection = There are no entries in the system from whi the_range_class_does_not_exist= The range class for this property does not exist in the system. editing_prohibited = This property is currently configured to prohibit editing. confirm_entry_deletion_from = Are you sure you want to delete the following entry from +confirm_individual_deletion = Are you sure you want to delete the following individual? edit_date_time_value = Edit Date/Time Value create_date_time_value = Create Date/Time Value diff --git a/webapp/src/main/webapp/templates/freemarker/body/accounts/userAccounts-createPassword.ftl b/webapp/src/main/webapp/templates/freemarker/body/accounts/userAccounts-createPassword.ftl index a3ba4273c..126a2626f 100644 --- a/webapp/src/main/webapp/templates/freemarker/body/accounts/userAccounts-createPassword.ftl +++ b/webapp/src/main/webapp/templates/freemarker/body/accounts/userAccounts-createPassword.ftl @@ -26,7 +26,7 @@
- + diff --git a/webapp/src/main/webapp/templates/freemarker/body/accounts/userAccounts-resetPassword.ftl b/webapp/src/main/webapp/templates/freemarker/body/accounts/userAccounts-resetPassword.ftl index cf11f0a72..645908854 100644 --- a/webapp/src/main/webapp/templates/freemarker/body/accounts/userAccounts-resetPassword.ftl +++ b/webapp/src/main/webapp/templates/freemarker/body/accounts/userAccounts-resetPassword.ftl @@ -26,7 +26,7 @@
- + 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 000000000..22bb8bea1 --- /dev/null +++ b/webapp/src/main/webapp/templates/freemarker/edit/forms/confirmDeleteIndividualForm.ftl @@ -0,0 +1,35 @@ +<#-- $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/edit/forms/defaultFormScripts.ftl b/webapp/src/main/webapp/templates/freemarker/edit/forms/defaultFormScripts.ftl index 0405d2f58..b623485af 100644 --- a/webapp/src/main/webapp/templates/freemarker/edit/forms/defaultFormScripts.ftl +++ b/webapp/src/main/webapp/templates/freemarker/edit/forms/defaultFormScripts.ftl @@ -39,7 +39,7 @@ theme_advanced_resizing : true, height : "${height}", width : "${width}", - valid_elements : "a[href|name|title],br,p,i,em,cite,strong/b,u,sub,sup,ul,ol,li", + valid_elements : "tr[*],td[*],tbody[*],table[*],a[href|name|title],br,p[style],i,em,cite,strong/b,u,sub,sup,ul,ol,li,h1[dir|style|id],h2[dir|style|id],h3[dir|style|id],h4,h5,h6,div[style|class],span[dir|style|class]", fix_list_elements : true, fix_nesting : true, cleanup_on_startup : true, @@ -49,7 +49,7 @@ paste_use_dialog : false, paste_auto_cleanup_on_paste : true, paste_convert_headers_to_strong : true, - paste_strip_class_attributes : "all", + paste_strip_class_attributes : "mso", paste_remove_spans : true, paste_remove_styles : true, paste_retain_style_properties : "" 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 3e5223fc6..34b00e51c 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> diff --git a/webapp/src/main/webapp/templates/freemarker/page/partials/identity.ftl b/webapp/src/main/webapp/templates/freemarker/page/partials/identity.ftl index 887fdb158..b94e0cd8f 100644 --- a/webapp/src/main/webapp/templates/freemarker/page/partials/identity.ftl +++ b/webapp/src/main/webapp/templates/freemarker/page/partials/identity.ftl @@ -31,7 +31,7 @@
${i18n().search_form} -