Commit 671c7059 authored by Michael Ritter's avatar Michael Ritter

Rework IMS blocking during audits

ace-am/
  - Migrate audit.max.block.time to ims.max.retry and ims.reset.timeout
  - Update settings page to display new ims blocking settings
  - Update css and layout for settings servlet
  - Various linting/dead code removal
ace-ims-api/
  - Create IMSResult to capture communication with the IMS
  - Move IMS blocking off of reflection and use a Supplier to wrap the
    soap request
  - Add documentation to ImmediateTokenRequestBatch and TokenValidator
parent 8b705921
......@@ -5,7 +5,6 @@
<artifactId>ace</artifactId>
<groupId>edu.umiacs.ace</groupId>
<version>1.14-SNAPSHOT</version>
<relativePath>../pom.xml</relativePath>
</parent>
<artifactId>ace-am</artifactId>
<name>ace-am</name>
......@@ -105,7 +104,7 @@
<scope>provided</scope>
</dependency>
<dependency>
<groupId>jstl</groupId>
<groupId>javax.servlet</groupId>
<artifactId>jstl</artifactId>
<version>1.1.2</version>
</dependency>
......@@ -196,34 +195,8 @@
<artifactId>irods-api</artifactId>
<version>1.6</version>
</dependency>
<!--<dependency>
<groupId>org.apache.jackrabbit</groupId>
<artifactId>jackrabbit-core</artifactId>
<version>2.2.7</version>
<exclusions>
<exclusion>
<artifactId>slf4j-api</artifactId>
<groupId>org.slf4j</groupId>
</exclusion>
<exclusion>
<artifactId>jcl-over-slf4j</artifactId>
<groupId>org.slf4j</groupId>
</exclusion>
</exclusions>
</dependency> -->
<!-- web services
<dependency>
<groupId>com.sun.jersey</groupId>
<artifactId>jersey-server</artifactId>
<version>1.6</version>
</dependency>
<dependency>
<groupId>com.sun.jersey</groupId>
<artifactId>jersey-json</artifactId>
<version>1.6</version>
</dependency>
-->
<!-- web services -->
<dependency>
<groupId>org.glassfish.jersey.containers</groupId>
<artifactId>jersey-container-servlet</artifactId>
......@@ -234,12 +207,6 @@
<artifactId>jersey-media-json-jackson</artifactId>
<version>2.24</version>
</dependency>
<dependency>
<groupId>edu.umiacs.ace</groupId>
<artifactId>ace-ims-ws</artifactId>
<version>1.14-SNAPSHOT</version>
<type>jar</type>
</dependency>
<dependency>
<groupId>org.apache.httpcomponents</groupId>
<artifactId>httpclient</artifactId>
......
......@@ -34,22 +34,23 @@ import edu.umiacs.ace.driver.AuditIterable;
import edu.umiacs.ace.driver.DriverStateBean;
import edu.umiacs.ace.driver.DriverStateBean.State;
import edu.umiacs.ace.driver.FileBean;
import edu.umiacs.ace.driver.filter.PathFilter;
import edu.umiacs.ace.driver.StorageDriver;
import edu.umiacs.ace.monitor.core.MonitoredItem;
import edu.umiacs.ace.monitor.core.Collection;
import edu.umiacs.ace.driver.QueryThrottle;
import edu.umiacs.ace.driver.StateBeanDigestListener;
import edu.umiacs.ace.driver.StorageDriver;
import edu.umiacs.ace.driver.filter.PathFilter;
import edu.umiacs.ace.monitor.core.Collection;
import edu.umiacs.ace.monitor.core.MonitoredItem;
import edu.umiacs.ace.util.HashValue;
import edu.umiacs.ace.util.ThreadedDigestStream;
import edu.umiacs.ace.util.ThrottledInputStream;
import edu.umiacs.io.IO;
import edu.umiacs.util.Strings;
import org.apache.log4j.Logger;
import javax.persistence.EntityManager;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.security.DigestInputStream;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.ArrayList;
......@@ -58,12 +59,10 @@ import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Queue;
import javax.persistence.EntityManager;
import org.apache.log4j.Logger;
/**
* Storage driver for accessing files stored on a local file system (ie, java.io.File)
*
*
* @author toaster
*/
public class LocalFileAccess extends StorageDriver {
......@@ -138,18 +137,19 @@ public class LocalFileAccess extends StorageDriver {
class MyIterator implements Iterator<FileBean> {
private FileBean next;
private Queue<File> dirsToProcess = new LinkedList<File>();
private Queue<File> filesToProcess = new LinkedList<File>();
private Queue<File> dirsToProcess = new LinkedList<>();
private Queue<File> filesToProcess = new LinkedList<>();
private MessageDigest digest;
// private byte[] buffer = new byte[BLOCK_SIZE];
private File rootFile;
private PathFilter filter;
private DriverStateBean statebean;
private ThreadedDigestStream reader;
private boolean cancel = false;
public MyIterator(MonitoredItem[] startPath, PathFilter filter,
String digestAlgorithm, DriverStateBean statebean) {
public MyIterator(MonitoredItem[] startPath,
PathFilter filter,
String digestAlgorithm,
DriverStateBean statebean) {
this.statebean = statebean;
this.filter = filter;
try {
......@@ -163,8 +163,7 @@ public class LocalFileAccess extends StorageDriver {
if (startPath != null) {
for (MonitoredItem mi : startPath) {
File startFile;
startFile = new File(
getCollection().getDirectory() + mi.getPath());
startFile = new File(getCollection().getDirectory() + mi.getPath());
if (startFile.isDirectory()) {
dirsToProcess.add(startFile);
......@@ -224,13 +223,13 @@ public class LocalFileAccess extends StorageDriver {
if (fileList == null) {
LOG.info("Could not read directory, skipping: " + directory);
} else {
for (File f : directory.listFiles()) {
for (File f : fileList) {
LOG.trace("Found item " + f);
if ( f.isDirectory() &&
if ( f.isDirectory() &&
filter.process(extractPathList(f), true)) {
LOG.trace("Adding matching directory: " + f);
dirsToProcess.add(f);
} else if ( f.isFile() &&
} else if ( f.isFile() &&
filter.process(extractPathList(f), false)) {
LOG.trace("Adding matching file: " + f);
filesToProcess.add(f);
......@@ -254,11 +253,9 @@ public class LocalFileAccess extends StorageDriver {
int substrLength = rootFile.getPath().length();
// build directory path
List<String> dirPathList = new ArrayList<String>();
List<String> dirPathList = new ArrayList<>();
File currFile = file;
while (!currFile.equals(rootFile)) {
// LOG.trace("Adding dir to path: " + currFile.getPath().substring(
// substrLength));
String pathToAdd = currFile.getPath().substring(substrLength);
pathToAdd = pathToAdd.replace(File.separatorChar, '/');
dirPathList.add(pathToAdd);
......@@ -269,11 +266,7 @@ public class LocalFileAccess extends StorageDriver {
@SuppressWarnings("empty-statement")
private FileBean processFile(File file) {
DigestInputStream dis = null;
FileBean fb = new FileBean();
fb.setPathList(extractPathList(file));
LOG.trace("Processing file: " + file);
......@@ -307,7 +300,6 @@ public class LocalFileAccess extends StorageDriver {
fb.setError(true);
fb.setErrorMessage(Strings.exceptionAsString(ie));
} finally {
IO.release(dis);
statebean.setStateAndReset(State.IDLE);
if (cancel) {
return null;
......
......@@ -55,6 +55,8 @@ import java.util.Timer;
import java.util.TimerTask;
import java.util.stream.Collectors;
import static edu.umiacs.ace.monitor.settings.SettingsConstants.*;
/**
* Set the IMS for the AuditThread to use. Also, startup a background task
* to handle firing off monitor tasks.
......@@ -65,13 +67,6 @@ import java.util.stream.Collectors;
*/
public final class AuditConfigurationContext implements ServletContextListener {
//private static final String PARAM_IMS = "ims";
//private static final String PARAM_IMS_PORT = "ims.port";
//private static final String PARAM_IMS_TOKEN_CLASS = "ims.tokenclass";
//private static final String PARAM_IMS_SSL = "ims.ssl";
//private static final String PARAM_DISABLE_AUTO_AUDIT = "auto.audit.disable";
//private static final String PARAM_THROTTLE_MAXAUDIT = "throttle.maxaudit";
//private static final String PARAM_AUDIT_ONLY = "audit.only";
public static final String ATTRIBUTE_PAUSE = "pause";
private static final long HOUR = 1000 * 60 * 60;
private Timer checkTimer;
......@@ -98,68 +93,51 @@ public final class AuditConfigurationContext implements ServletContextListener {
ctx.setAttribute(ATTRIBUTE_PAUSE, pb);
// Invert the boolean because the PB checks if we're paused, not enabled
String enableAudits = resultMap.getOrDefault(
SettingsConstants.PARAM_AUTO_AUDIT_ENABLE,
SettingsConstants.autoAudit);
String enableAudits = resultMap.getOrDefault(PARAM_AUTO_AUDIT_ENABLE, autoAudit);
pb.setPaused(!Boolean.valueOf(enableAudits));
checkTimer = new Timer("Audit Check Timer");
checkTimer.schedule(new MyTimerTask(pb), 0, HOUR);
// set IMS for audit Thread from server parameter
AuditThreadFactory.setIMS(resultMap.getOrDefault(
SettingsConstants.PARAM_IMS,
SettingsConstants.ims));
AuditThreadFactory.setIMS(resultMap.getOrDefault(PARAM_IMS, ims));
String tokenClass = resultMap.getOrDefault(
SettingsConstants.PARAM_IMS_TOKEN_CLASS,
SettingsConstants.imsTokenClass);
String tokenClass = resultMap.getOrDefault(PARAM_IMS_TOKEN_CLASS, imsTokenClass);
AuditThreadFactory.setTokenClass(tokenClass);
String port = resultMap.getOrDefault(
SettingsConstants.PARAM_IMS_PORT,
SettingsConstants.imsPort);
String port = resultMap.getOrDefault(PARAM_IMS_PORT, imsPort);
if (Strings.isValidInt(port)) {
AuditThreadFactory.setImsPort(Integer.parseInt(port));
}
String auditOnly = resultMap.getOrDefault(
SettingsConstants.PARAM_AUDIT_ONLY,
SettingsConstants.auditOnly);
String auditOnly = resultMap.getOrDefault(PARAM_AUDIT_ONLY, SettingsConstants.auditOnly);
AuditThreadFactory.setAuditOnly(Boolean.valueOf(auditOnly));
// keep this off for now even though it isn't used
AuditThreadFactory.setAuditSampling(false);
String imsSsl = resultMap.getOrDefault(
SettingsConstants.PARAM_IMS_SSL,
SettingsConstants.imsSSL);
String imsSsl = resultMap.getOrDefault(PARAM_IMS_SSL, imsSSL);
AuditThreadFactory.setSSL(Boolean.valueOf(imsSsl));
String blocking = resultMap.getOrDefault(
SettingsConstants.PARAM_AUDIT_BLOCKING,
SettingsConstants.auditBlocking);
String blocking = resultMap.getOrDefault(PARAM_AUDIT_BLOCKING, auditBlocking);
AuditThreadFactory.setBlocking(Boolean.valueOf(blocking));
String blockTimeS = resultMap.getOrDefault(
SettingsConstants.PARAM_AUDIT_MAX_BLOCK_TIME,
SettingsConstants.auditMaxBlockTime);
int blockTime = 0;
if (Strings.isValidInt(blockTimeS)) {
blockTime = Integer.parseInt(blockTimeS);
int maxRetry = Integer.parseInt(imsMaxRetry);
String imsRetryString = resultMap.getOrDefault(PARAM_IMS_MAX_RETRY, imsMaxRetry);
if (Strings.isNonNegativeInt(imsRetryString)) {
maxRetry = Integer.parseInt(imsRetryString);
}
// Just in case...
if (blockTime < 0) {
blockTime = 0;
AuditThreadFactory.setImsRetryAttempts(maxRetry);
int resetTimeout = Integer.parseInt(imsResetTimeout);
String imsResetTimeout = resultMap.getOrDefault(
PARAM_IMS_RESET_TIMEOUT,
SettingsConstants.imsResetTimeout);
if (Strings.isNonNegativeInt(imsResetTimeout)) {
resetTimeout = Integer.parseInt(imsResetTimeout);
}
AuditThreadFactory.setMaxBlockTime(blockTime);
AuditThreadFactory.setImsResetTimeout(resetTimeout);
String maxAudit = resultMap.getOrDefault(
SettingsConstants.PARAM_THROTTLE_MAXAUDIT,
SettingsConstants.maxAudit);
if (Strings.isValidInt(maxAudit)) {
int audit = Integer.parseInt(maxAudit);
String maxAuditString = resultMap.getOrDefault(PARAM_THROTTLE_MAXAUDIT, maxAudit);
if (Strings.isValidInt(maxAuditString)) {
int audit = Integer.parseInt(maxAuditString);
if (audit > 0) {
AuditThreadFactory.setMaxAudits(audit);
}
......
......@@ -65,10 +65,10 @@ public class AuditThreadFactory {
private static int imsPort = 80;
private static String tokenClass = "SHA-256";
private static boolean auditOnly = false;
private static boolean auditSample = false;
private static boolean ssl = false;
private static boolean blocking = false;
private static int maxBlockTime = 0;
private static int imsRetryAttempts = 3;
private static int imsResetTimeout = 3000;
public static void setIMS( String ims ) {
if (Strings.isEmpty(ims)) {
......@@ -95,8 +95,8 @@ public class AuditThreadFactory {
}
public static void setImsPort(int imsPort) {
if (imsPort < 1 && imsPort > 32768) {
LOG.error("ims port must be between 1 and 32768, setting default");
if (imsPort < 1 || imsPort > 32768) {
LOG.warn("ims port must be between 1 and 32768, setting default");
imsPort = Integer.parseInt(SettingsConstants.imsPort);
}
AuditThreadFactory.imsPort = imsPort;
......@@ -106,27 +106,32 @@ public class AuditThreadFactory {
AuditThreadFactory.auditOnly = auditOnlyMode;
}
public static void setAuditSampling(boolean auditSampling ) {
AuditThreadFactory.auditSample = auditSampling;
public static int getImsRetryAttempts() {
return imsRetryAttempts;
}
public static void setBlocking(boolean blocking) {
AuditThreadFactory.blocking = blocking;
public static void setImsRetryAttempts(int attempts) {
if (attempts >= 0) {
imsRetryAttempts = attempts;
}
}
public static boolean isBlocking() {
return AuditThreadFactory.blocking;
public static int getImsResetTimeout() {
return imsResetTimeout;
}
public static void setMaxBlockTime(int maxBlockTime) {
if ( maxBlockTime < 0 ) {
maxBlockTime = 0;
public static void setImsResetTimeout(int timeout) {
if (timeout >= 0) {
imsResetTimeout = timeout;
}
AuditThreadFactory.maxBlockTime = maxBlockTime;
}
public static int getMaxBlockTime() {
return maxBlockTime;
public static void setBlocking(boolean blocking) {
AuditThreadFactory.blocking = blocking;
}
public static boolean isBlocking() {
return AuditThreadFactory.blocking;
}
public static boolean isAuditing() {
......
......@@ -197,9 +197,13 @@ public final class AuditTokens extends Thread implements CancelCallback {
imsPort,
AuditThreadFactory.useSSL(),
AuditThreadFactory.isBlocking(),
AuditThreadFactory.getMaxBlockTime());
AuditThreadFactory.getImsRetryAttempts(),
AuditThreadFactory.getImsResetTimeout());
callback = new TokenAuditCallback(itemMap, this, collection, session);
validator = ims.createTokenValidator(callback, 1000, 5000,
validator = ims.createTokenValidator(collection.getId().toString(),
callback,
1000,
5000,
digest);
} catch (Throwable e) {
EntityManager em;
......
......@@ -57,14 +57,14 @@ public final class TokenAuditCallback implements ValidationCallback {
private long totalErrors = 0;
private long validTokens = 0;
private CancelCallback cancel;
// private Collection collection;
LogEventManager logManager;
private LogEventManager logManager;
public TokenAuditCallback( Map<AceToken, MonitoredItem> itemMap,
CancelCallback callback, Collection collection, long session ) {
public TokenAuditCallback(Map<AceToken, MonitoredItem> itemMap,
CancelCallback callback,
Collection collection,
long session ) {
this.itemMap = itemMap;
this.cancel = callback;
// this.collection = collection;
logManager = new LogEventManager(session, collection);
}
......@@ -81,12 +81,8 @@ public final class TokenAuditCallback implements ValidationCallback {
totalErrors++;
LOG.error("Exception throw registering", throwable);
EntityManager em = PersistUtil.getEntityManager();
// EntityTransaction trans = em.getTransaction();
// trans.begin();
String msg = "Exception in batch thread" + Strings.exceptionAsString(throwable);
logManager.persistCollectionEvent(LogEnum.SYSTEM_ERROR, msg, em);
// lem.abortSite(collection, "Exception in batch thread", throwable);
// trans.commit();
em.close();
cancel.cancel();
}
......@@ -106,10 +102,7 @@ public final class TokenAuditCallback implements ValidationCallback {
if ( !token.getValid() ) {
token.setValid(true);
// LogEventManager lem = new LogEventManager(session, collection);
// String path = response.getName();
em.persist(logManager.createItemEvent(LogEnum.TOKEN_VALID, item.getPath()));
// em.persist(lem.validToken(path, collection));
}
if ( item.getState() == 'I' || item.getState() == 'R' ) {
......
......@@ -6,32 +6,36 @@
package edu.umiacs.ace.monitor.settings;
import edu.umiacs.ace.util.EntityManagerServlet;
import java.io.IOException;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import javax.persistence.EntityManager;
import javax.servlet.RequestDispatcher;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.apache.commons.fileupload.FileItemIterator;
import org.apache.commons.fileupload.FileItemStream;
import org.apache.commons.fileupload.FileUploadException;
import org.apache.commons.fileupload.servlet.ServletFileUpload;
import org.apache.commons.fileupload.util.Streams;
import org.apache.log4j.Logger;
import javax.persistence.EntityManager;
import javax.servlet.RequestDispatcher;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.List;
/**
*
* @author shake
*/
public class AddSettingServlet extends EntityManagerServlet{
public class AddSettingServlet extends EntityManagerServlet {
private static final Logger log = Logger.getLogger(AddSettingServlet.class);
@Override
protected void processRequest(HttpServletRequest request, HttpServletResponse
response, EntityManager em) throws ServletException, IOException {
List<SettingsParameter> customSettings = new ArrayList<SettingsParameter>();
protected void processRequest(HttpServletRequest request,
HttpServletResponse response,
EntityManager em) throws ServletException, IOException {
List<SettingsParameter> customSettings = new ArrayList<>();
ServletFileUpload su = new ServletFileUpload();
try {
......@@ -58,7 +62,7 @@ public class AddSettingServlet extends EntityManagerServlet{
}
} catch (FileUploadException ex) {
// Logger.getLogger(SettingsServlet.class.getName()).log(Level.SEVERE, null, ex);
log.warn("Exception in AddSettingServlet", ex);
}
SettingsUtil.updateSettings(customSettings);
......
......@@ -2,7 +2,9 @@ package edu.umiacs.ace.monitor.settings;
import edu.umiacs.ace.util.PersistUtil;
import edu.umiacs.sql.SQL;
import edu.umiacs.util.Check;
import org.apache.log4j.Logger;
import org.apache.log4j.NDC;
import javax.servlet.ServletContextEvent;
import javax.servlet.ServletContextListener;
......@@ -12,45 +14,77 @@ import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import static edu.umiacs.ace.monitor.settings.SettingsConstants.PARAM_IMS_MAX_RETRY;
import static edu.umiacs.ace.monitor.settings.SettingsConstants.PARAM_IMS_RESET_TIMEOUT;
import static edu.umiacs.ace.monitor.settings.SettingsConstants.imsMaxRetry;
import static edu.umiacs.ace.monitor.settings.SettingsConstants.imsResetTimeout;
/**
* Context listener to migrate from auto.audit.disable to auto.audit.enable
*
* Context listener to migrate audit/ims settings
* auto.audit.disable to auto.audit.enable
* audit.max.block.time to ims.max.retry
* add ims.reset.timeout
* <p>
* Created by shake on 4/5/17.
*/
public class AutoAuditMigrationContextListener implements ServletContextListener {
private static final Logger LOG = Logger.getLogger(AutoAuditMigrationContextListener.class);
private ResultSet set;
private Connection conn;
private PreparedStatement statement;
@Override
public void contextInitialized(ServletContextEvent servletContextEvent) {
Connection conn = null;
NDC.push("[MIGRATION] ");
DataSource ds = PersistUtil.getDataSource();
try {
conn = ds.getConnection();
migrate(conn);
try (Connection conn = ds.getConnection()) {
migrateAutoAudit(conn);
migrateAuditBlocking(conn);
} catch (Exception e) {
LOG.error("[MIGRATION] Error migrating audo.audit.disable setting", e);
LOG.error("Error migrating audit settings", e);
} finally {
release();
NDC.pop();
}
}
private void migrate(Connection conn) throws SQLException {
PreparedStatement statement = conn.prepareStatement("SELECT id, value, custom FROM system_settings WHERE attr = 'auto.audit.disable'");
ResultSet set = statement.executeQuery();
while (set.next()) {
Long id = set.getLong(1);
Boolean value = Boolean.valueOf(set.getString(2));
Boolean custom = set.getBoolean(3);
if (!custom) {
LOG.info("[MIGRATION] Found auto.audit setting to migrate from " + value + " -> " + !value);
createNew(conn, !value);
/**
* Migrate from auto.audit.disable to auto.audit.enable
*
* @param conn The database connection
* @throws SQLException if there's an exception communicating with the database
*/
private void migrateAutoAudit(Connection conn) throws SQLException {
String query = "SELECT id, value, custom FROM system_settings WHERE attr = 'auto.audit.disable'";
try (PreparedStatement statement = conn.prepareStatement(query);
ResultSet set = statement.executeQuery()) {
while (set.next()) {
Long id = set.getLong(1);
Boolean value = Boolean.valueOf(set.getString(2));
Boolean custom = set.getBoolean(3);
if (!custom) {
LOG.info("Found auto.audit setting to migrate from " + value + " -> " + !value);
createNew(conn, "auto.audit.enable", String.valueOf(!value));
delete(conn, id);
}
}
}
}
/**
* Check if audit.max.block.time exists. If true, add ims.max.retry and ims.reset.timeout and
* remove audit.max.block.time
*
* @param conn The database connection
* @throws SQLException if there's an exception communicating with the database
*/
private void migrateAuditBlocking(Connection conn) throws SQLException {
String query = "SELECT id FROM system_settings WHERE attr = 'audit.max.block.time'";
try (PreparedStatement statement = conn.prepareStatement(query);
ResultSet set = statement.executeQuery()) {
while (set.next()) {
Long id = set.getLong(1);
LOG.info("Found audit.max.block.time setting to migrate to ims.max.retry");
createNew(conn, PARAM_IMS_MAX_RETRY, imsMaxRetry);
createNew(conn, PARAM_IMS_RESET_TIMEOUT, imsResetTimeout);
delete(conn, id);
}
}
......@@ -62,21 +96,14 @@ public class AutoAuditMigrationContextListener implements ServletContextListener
SQL.release(statement);
}
private void createNew(Connection conn, Boolean value) throws SQLException {
PreparedStatement statement = conn.prepareStatement("INSERT INTO system_settings VALUES (DEFAULT, 'auto.audit.enable','" + value.toString() + "',0)");
statement.executeUpdate();
SQL.release(statement);
}
private void createNew(Connection conn, String attr, String value) throws SQLException {
Check.notNull("attr", attr);
Check.notNull("value", value);
private void release() {
if (conn != null) {
SQL.release(conn);
}
if (statement != null) {
SQL.release(statement);
}
if (set != null) {
SQL.release(set);
String query = "INSERT INTO system_settings VALUES (DEFAULT, '%s', '%s', 0)";
try (PreparedStatement statement =
conn.prepareStatement(String.format(query, attr, value))) {
statement.executeUpdate();