diff --git a/Core/nbproject/project.xml b/Core/nbproject/project.xml index 57cbc58c25..dba056009d 100644 --- a/Core/nbproject/project.xml +++ b/Core/nbproject/project.xml @@ -329,6 +329,7 @@ org.sleuthkit.autopsy.guiutils org.sleuthkit.autopsy.healthmonitor org.sleuthkit.autopsy.ingest + org.sleuthkit.autopsy.ingest.events org.sleuthkit.autopsy.keywordsearchservice org.sleuthkit.autopsy.menuactions org.sleuthkit.autopsy.modules.encryptiondetection @@ -499,7 +500,7 @@ ext/xmpcore-5.1.3.jar release/modules/ext/xmpcore-5.1.3.jar - + ext/SparseBitSet-1.1.jar release/modules/ext/SparseBitSet-1.1.jar diff --git a/ImageGallery/src/org/sleuthkit/autopsy/imagegallery/ImageGalleryController.java b/ImageGallery/src/org/sleuthkit/autopsy/imagegallery/ImageGalleryController.java index cca4ec0f93..76c3626512 100644 --- a/ImageGallery/src/org/sleuthkit/autopsy/imagegallery/ImageGalleryController.java +++ b/ImageGallery/src/org/sleuthkit/autopsy/imagegallery/ImageGalleryController.java @@ -23,8 +23,10 @@ import com.google.common.util.concurrent.MoreExecutors; import com.google.common.util.concurrent.ThreadFactoryBuilder; import java.beans.PropertyChangeEvent; import java.beans.PropertyChangeListener; +import java.util.HashSet; import java.util.List; import java.util.Objects; +import java.util.Set; import java.util.concurrent.Executors; import java.util.concurrent.TimeUnit; import java.util.logging.Level; @@ -53,11 +55,13 @@ import javafx.scene.layout.Region; import javafx.scene.layout.StackPane; import javafx.scene.paint.Color; import javax.annotation.Nullable; +import javax.swing.JOptionPane; import javax.swing.SwingUtilities; import org.netbeans.api.progress.ProgressHandle; import org.openide.util.Cancellable; import org.openide.util.NbBundle; import org.sleuthkit.autopsy.casemodule.Case; +import org.sleuthkit.autopsy.casemodule.Case.CaseType; import org.sleuthkit.autopsy.casemodule.NoCurrentCaseException; import org.sleuthkit.autopsy.casemodule.events.ContentTagAddedEvent; import org.sleuthkit.autopsy.casemodule.events.ContentTagDeletedEvent; @@ -66,6 +70,7 @@ import org.sleuthkit.autopsy.coreutils.History; import org.sleuthkit.autopsy.coreutils.Logger; import org.sleuthkit.autopsy.coreutils.MessageNotifyUtil; import org.sleuthkit.autopsy.coreutils.ThreadConfined; +import org.sleuthkit.autopsy.events.AutopsyEvent; import org.sleuthkit.autopsy.imagegallery.actions.UndoRedoManager; import org.sleuthkit.autopsy.imagegallery.datamodel.CategoryManager; import org.sleuthkit.autopsy.imagegallery.datamodel.DrawableDB; @@ -77,9 +82,11 @@ import org.sleuthkit.autopsy.imagegallery.datamodel.grouping.GroupViewState; import org.sleuthkit.autopsy.imagegallery.gui.NoGroupsDialog; import org.sleuthkit.autopsy.imagegallery.gui.Toolbar; import org.sleuthkit.autopsy.ingest.IngestManager; +import org.sleuthkit.autopsy.ingest.events.DataSourceAnalysisCompletedEvent; import org.sleuthkit.autopsy.modules.filetypeid.FileTypeDetector; import org.sleuthkit.datamodel.AbstractFile; import org.sleuthkit.datamodel.Content; +import org.sleuthkit.datamodel.DataSource; import org.sleuthkit.datamodel.SleuthkitCase; import org.sleuthkit.datamodel.TskCoreException; import org.sleuthkit.datamodel.TskData; @@ -213,11 +220,15 @@ public final class ImageGalleryController { listeningEnabled.addListener((observable, oldValue, newValue) -> { try { - //if we just turned on listening and a case is open and that case is not up to date - if (newValue && !oldValue && ImageGalleryModule.isDrawableDBStale(Case.getCurrentCaseThrows())) { + // if we just turned on listening and a single-user case is open and that case is not up to date, then rebuild it + // For multiuser cases, we defer DB rebuild till the user actually opens Image Gallery + if ( newValue && !oldValue && + ImageGalleryModule.isDrawableDBStale(Case.getCurrentCaseThrows()) && + (Case.getCurrentCaseThrows().getCaseType() == CaseType.SINGLE_USER_CASE) ) { //populate the db - queueDBTask(new CopyAnalyzedFiles(instance, db, sleuthKitCase)); + this.rebuildDB(); } + } catch (NoCurrentCaseException ex) { LOGGER.log(Level.WARNING, "Exception while getting open case.", ex); } @@ -386,6 +397,14 @@ public final class ImageGalleryController { } } + /** + * Rebuilds the DrawableDB database. + * + */ + public void rebuildDB() { + queueDBTask(new CopyAnalyzedFiles(instance, db, sleuthKitCase)); + } + /** * reset the state of the controller (eg if the case is closed) */ @@ -411,6 +430,56 @@ public final class ImageGalleryController { db = null; } + /** + * Checks if the datasources table in drawable DB is stale. + * + * @return true if datasources table is stale + */ + boolean isDataSourcesTableStale() { + + // no current case open to check + if ((null == getDatabase()) || (null == getSleuthKitCase())) { + return false; + } + + try { + Set knownDataSourceIds= getDatabase().getDataSourceIds(); + List dataSources = getSleuthKitCase().getDataSources(); + Set caseDataSourceIds = new HashSet<>(); + dataSources.forEach((dataSource) -> { + caseDataSourceIds.add(dataSource.getId()); + }); + + return !(knownDataSourceIds.containsAll(caseDataSourceIds) && caseDataSourceIds.containsAll(knownDataSourceIds)); + } + catch (TskCoreException ex) { + LOGGER.log(Level.SEVERE, "Image Gallery failed to check if datasources table is stale.", ex); + return false; + } + + } + + /** + * Update the datasources table in drawable DB. + * + */ + private void updateDataSourcesTable() { + // no current case open to update + if ((null == getDatabase()) || (null == getSleuthKitCase())) { + return; + } + + try { + List dataSources = getSleuthKitCase().getDataSources(); + dataSources.forEach((dataSource) -> { + getDatabase().insertDataSource(dataSource.getId()); + }); + } + catch (TskCoreException ex) { + LOGGER.log(Level.SEVERE, "Image Gallery failed to update data_sources table.", ex); + } + } + synchronized private void shutDownDBExecutor() { if (dbExecutor != null) { dbExecutor.shutdownNow(); @@ -483,8 +552,8 @@ public final class ImageGalleryController { void onStart() { Platform.setImplicitExit(false); LOGGER.info("setting up ImageGallery listeners"); //NON-NLS - //TODO can we do anything usefull in an InjestJobEventListener? - //IngestManager.getInstance().addIngestJobEventListener((PropertyChangeEvent evt) -> {}); + + IngestManager.getInstance().addIngestJobEventListener( new IngestJobEventListener()); IngestManager.getInstance().addIngestModuleEventListener(new IngestModuleEventListener()); Case.addPropertyChangeListener(new CaseEventListener()); } @@ -659,12 +728,12 @@ public final class ImageGalleryController { "BulkTask.errPopulating.errMsg=There was an error populating Image Gallery database."}) /* Base abstract class for various methods of copying data into the Image gallery DB */ abstract static private class BulkTransferTask extends BackgroundTask { - + static private final String FILE_EXTENSION_CLAUSE = - "(name LIKE '%." //NON-NLS - + String.join("' OR name LIKE '%.", FileTypeUtils.getAllSupportedExtensions()) //NON-NLS - + "')"; - + "(extension LIKE '" //NON-NLS + + String.join("' OR extension LIKE '", FileTypeUtils.getAllSupportedExtensions()) //NON-NLS + + "') "; + static private final String MIMETYPE_CLAUSE = "(mime_type LIKE '" //NON-NLS + String.join("' OR mime_type LIKE '", FileTypeUtils.getAllSupportedMimeTypes()) //NON-NLS @@ -683,6 +752,8 @@ public final class ImageGalleryController { final SleuthkitCase tskCase; ProgressHandle progressHandle; + + private boolean taskCompletionStatus; BulkTransferTask(ImageGalleryController controller, DrawableDB taskDB, SleuthkitCase tskCase) { this.controller = controller; @@ -712,16 +783,22 @@ public final class ImageGalleryController { progressHandle.switchToDeterminate(files.size()); updateProgress(0.0); + + taskCompletionStatus = true; //do in transaction DrawableDB.DrawableTransaction tr = taskDB.beginTransaction(); int workDone = 0; for (final AbstractFile f : files) { - if (isCancelled() || Thread.interrupted()) { + if (isCancelled()) { LOGGER.log(Level.WARNING, "Task cancelled: not all contents may be transfered to drawable database."); //NON-NLS progressHandle.finish(); break; } + + if (Thread.interrupted()) { + LOGGER.log(Level.WARNING, "BulkTransferTask interrupted. Ignoring it to update the contents of drawable database."); //NON-NLS + } processFile(f, tr); @@ -750,10 +827,14 @@ public final class ImageGalleryController { updateMessage(""); updateProgress(-1.0); } - cleanup(true); + cleanup(taskCompletionStatus); } abstract ProgressHandle getInitialProgressHandle(); + + protected void setTaskCompletionStatus(boolean status) { + taskCompletionStatus = status; + } } /** @@ -774,6 +855,7 @@ public final class ImageGalleryController { @Override protected void cleanup(boolean success) { + controller.updateDataSourcesTable(); controller.setStale(!success); } @@ -783,7 +865,7 @@ public final class ImageGalleryController { } @Override - void processFile(AbstractFile f, DrawableDB.DrawableTransaction tr) { + void processFile(AbstractFile f, DrawableDB.DrawableTransaction tr) throws TskCoreException { final boolean known = f.getKnown() == TskData.FileKnown.KNOWN; if (known) { @@ -791,13 +873,20 @@ public final class ImageGalleryController { } else { try { - if (FileTypeUtils.hasDrawableMIMEType(f)) { //supported mimetype => analyzed + //supported mimetype => analyzed + if ( null != f.getMIMEType() && FileTypeUtils.hasDrawableMIMEType(f)) { taskDB.updateFile(DrawableFile.create(f, true, false), tr); - } else { //unsupported mimtype => analyzed but shouldn't include + } + else { //unsupported mimtype => analyzed but shouldn't include + + // if mimetype of the file hasn't been ascertained, ingest might not have completed yet. + if (null == f.getMIMEType()) { + this.setTaskCompletionStatus(false); + } taskDB.removeFile(f.getId(), tr); } } catch (FileTypeDetector.FileTypeDetectorInitException ex) { - throw new RuntimeException(ex); + throw new TskCoreException("Failed to initialize FileTypeDetector.", ex); } } } @@ -890,28 +979,32 @@ public final class ImageGalleryController { AbstractFile file = (AbstractFile) evt.getNewValue(); - if (isListeningEnabled()) { - if (file.isFile()) { - try { - synchronized (ImageGalleryController.this) { - if (ImageGalleryModule.isDrawableAndNotKnown(file)) { - //this file should be included and we don't already know about it from hash sets (NSRL) - queueDBTask(new UpdateFileTask(file, db)); - } else if (FileTypeUtils.getAllSupportedExtensions().contains(file.getNameExtension())) { - //doing this check results in fewer tasks queued up, and faster completion of db update - //this file would have gotten scooped up in initial grab, but actually we don't need it - queueDBTask(new RemoveFileTask(file, db)); + // only process individual files in realtime on the node that is running the ingest + // on a remote node, image files are processed enblock when ingest is complete + if (((AutopsyEvent) evt).getSourceType() == AutopsyEvent.SourceType.LOCAL) { + if (isListeningEnabled()) { + if (file.isFile()) { + try { + synchronized (ImageGalleryController.this) { + if (ImageGalleryModule.isDrawableAndNotKnown(file)) { + //this file should be included and we don't already know about it from hash sets (NSRL) + queueDBTask(new UpdateFileTask(file, db)); + } else if (FileTypeUtils.getAllSupportedExtensions().contains(file.getNameExtension())) { + //doing this check results in fewer tasks queued up, and faster completion of db update + //this file would have gotten scooped up in initial grab, but actually we don't need it + queueDBTask(new RemoveFileTask(file, db)); + } } + } catch (TskCoreException | FileTypeDetector.FileTypeDetectorInitException ex) { + //TODO: What to do here? + LOGGER.log(Level.SEVERE, "Unable to determine if file is drawable and not known. Not making any changes to DB", ex); //NON-NLS + MessageNotifyUtil.Notify.error("Image Gallery Error", + "Unable to determine if file is drawable and not known. Not making any changes to DB. See the logs for details."); } - } catch (TskCoreException | FileTypeDetector.FileTypeDetectorInitException ex) { - //TODO: What to do here? - LOGGER.log(Level.SEVERE, "Unable to determine if file is drawable and not known. Not making any changes to DB", ex); //NON-NLS - MessageNotifyUtil.Notify.error("Image Gallery Error", - "Unable to determine if file is drawable and not known. Not making any changes to DB. See the logs for details."); } + } else { //TODO: keep track of what we missed for later + setStale(true); } - } else { //TODO: keep track of what we missed for later - setStale(true); } break; } @@ -943,12 +1036,14 @@ public final class ImageGalleryController { } break; case DATA_SOURCE_ADDED: - //copy all file data to drawable databse - Content newDataSource = (Content) evt.getNewValue(); - if (isListeningEnabled()) { - queueDBTask(new PrePopulateDataSourceFiles(newDataSource, ImageGalleryController.this, getDatabase(), getSleuthKitCase())); - } else {//TODO: keep track of what we missed for later - setStale(true); + //For a data source added on the local node, prepopulate all file data to drawable database + if (((AutopsyEvent) evt).getSourceType() == AutopsyEvent.SourceType.LOCAL) { + Content newDataSource = (Content) evt.getNewValue(); + if (isListeningEnabled()) { + queueDBTask(new PrePopulateDataSourceFiles(newDataSource, ImageGalleryController.this, getDatabase(), getSleuthKitCase())); + } else {//TODO: keep track of what we missed for later + setStale(true); + } } break; case CONTENT_TAG_ADDED: @@ -966,4 +1061,62 @@ public final class ImageGalleryController { } } } + + + /** + * Listener for Ingest Job events. + */ + private class IngestJobEventListener implements PropertyChangeListener { + + @NbBundle.Messages({ + "ImageGalleryController.dataSourceAnalyzed.confDlg.msg= A new data source was added and finished ingest.\n" + + "The image / video database may be out of date. " + + "Do you want to update the database with ingest results?\n", + "ImageGalleryController.dataSourceAnalyzed.confDlg.title=Image Gallery" + }) + @Override + public void propertyChange(PropertyChangeEvent evt) { + String eventName = evt.getPropertyName(); + if ( eventName.equals(IngestManager.IngestJobEvent.DATA_SOURCE_ANALYSIS_COMPLETED.toString())) { + if (((AutopsyEvent) evt).getSourceType() == AutopsyEvent.SourceType.REMOTE) { + // A remote node added a new data source and just finished ingest on it. + //drawable db is stale, and if ImageGallery is open, ask user what to do + setStale(true); + + SwingUtilities.invokeLater(() -> { + if (isListeningEnabled() && ImageGalleryTopComponent.isImageGalleryOpen()) { + + int answer = JOptionPane.showConfirmDialog(ImageGalleryTopComponent.getTopComponent(), + Bundle.ImageGalleryController_dataSourceAnalyzed_confDlg_msg(), + Bundle.ImageGalleryController_dataSourceAnalyzed_confDlg_title(), + JOptionPane.YES_NO_CANCEL_OPTION, JOptionPane.WARNING_MESSAGE); + + switch (answer) { + case JOptionPane.YES_OPTION: + rebuildDB(); + break; + case JOptionPane.NO_OPTION: + case JOptionPane.CANCEL_OPTION: + default: + break; //do nothing + } + } + }); + } else { + // received event from local node + // add the datasource to drawable db + long dsObjId = 0; + DataSourceAnalysisCompletedEvent event = (DataSourceAnalysisCompletedEvent)evt; + if(event.getDataSource() != null) { + dsObjId = event.getDataSource().getId(); + db.insertDataSource(dsObjId); + // All files for the data source have been analyzed. + setStale(false); + } else { + LOGGER.log(Level.SEVERE, "DataSourceAnalysisCompletedEvent does not contain a dataSource object"); //NON-NLS + } + } + } + } + } } diff --git a/ImageGallery/src/org/sleuthkit/autopsy/imagegallery/ImageGalleryModule.java b/ImageGallery/src/org/sleuthkit/autopsy/imagegallery/ImageGalleryModule.java index a5f13c1d9a..a6b57e5779 100644 --- a/ImageGallery/src/org/sleuthkit/autopsy/imagegallery/ImageGalleryModule.java +++ b/ImageGallery/src/org/sleuthkit/autopsy/imagegallery/ImageGalleryModule.java @@ -86,7 +86,9 @@ public class ImageGalleryModule { public static boolean isDrawableDBStale(Case c) { if (c != null) { String stale = new PerCaseProperties(c).getConfigSetting(ImageGalleryModule.MODULE_NAME, PerCaseProperties.STALE); - return StringUtils.isNotBlank(stale) ? Boolean.valueOf(stale) : true; + + return ( ImageGalleryController.getDefault().isDataSourcesTableStale() || + (StringUtils.isNotBlank(stale) ? Boolean.valueOf(stale) : true) ); } else { return false; } diff --git a/ImageGallery/src/org/sleuthkit/autopsy/imagegallery/ImageGalleryTopComponent.java b/ImageGallery/src/org/sleuthkit/autopsy/imagegallery/ImageGalleryTopComponent.java index a6ce70c3ac..ce06c52893 100644 --- a/ImageGallery/src/org/sleuthkit/autopsy/imagegallery/ImageGalleryTopComponent.java +++ b/ImageGallery/src/org/sleuthkit/autopsy/imagegallery/ImageGalleryTopComponent.java @@ -39,6 +39,7 @@ import org.openide.windows.RetainLocation; import org.openide.windows.TopComponent; import org.openide.windows.WindowManager; import org.sleuthkit.autopsy.coreutils.Logger; +import org.sleuthkit.autopsy.coreutils.ThreadConfined; import org.sleuthkit.autopsy.imagegallery.gui.StatusBar; import org.sleuthkit.autopsy.imagegallery.gui.SummaryTablePane; import org.sleuthkit.autopsy.imagegallery.gui.Toolbar; @@ -88,6 +89,29 @@ public final class ImageGalleryTopComponent extends TopComponent implements Expl private VBox leftPane; private Scene myScene; + /** + * Returns whether the ImageGallery window is open or not. + * + * @return true, if Image gallery is opened, false otherwise + */ + public static boolean isImageGalleryOpen() { + + final TopComponent topComponent = WindowManager.getDefault().findTopComponent(PREFERRED_ID); + if (topComponent != null) { + return topComponent.isOpened(); + } + return false; + } + + /** + * Returns the top component window. + * + * @return Image gallery top component window, null if it's not open + */ + public static TopComponent getTopComponent() { + return WindowManager.getDefault().findTopComponent(PREFERRED_ID); + } + public static void openTopComponent() { //TODO:eventually move to this model, throwing away everything and rebuilding controller groupmanager etc for each case. // synchronized (OpenTimelineAction.class) { diff --git a/ImageGallery/src/org/sleuthkit/autopsy/imagegallery/actions/OpenAction.java b/ImageGallery/src/org/sleuthkit/autopsy/imagegallery/actions/OpenAction.java index e10a679792..2fbc9b3758 100644 --- a/ImageGallery/src/org/sleuthkit/autopsy/imagegallery/actions/OpenAction.java +++ b/ImageGallery/src/org/sleuthkit/autopsy/imagegallery/actions/OpenAction.java @@ -172,7 +172,18 @@ public final class OpenAction extends CallableSystemAction { switch (answer) { case JOptionPane.YES_OPTION: - ImageGalleryController.getDefault().setListeningEnabled(true); + + // For a single-user case, we favor user experience, and rebuild the database + // as soon as Image Gallery is enabled for the case. + // For a multi-user case, we favor overall performance and user experience, not every user may want to review images, + // so we rebuild the database only when a user launches Image Gallery + if (currentCase.getCaseType() == Case.CaseType.SINGLE_USER_CASE) { + ImageGalleryController.getDefault().setListeningEnabled(true); + } + else { + ImageGalleryController.getDefault().rebuildDB(); + } + //fall through case JOptionPane.NO_OPTION: ImageGalleryTopComponent.openTopComponent(); diff --git a/ImageGallery/src/org/sleuthkit/autopsy/imagegallery/datamodel/DrawableDB.java b/ImageGallery/src/org/sleuthkit/autopsy/imagegallery/datamodel/DrawableDB.java index 634a7dc784..a578641c22 100644 --- a/ImageGallery/src/org/sleuthkit/autopsy/imagegallery/datamodel/DrawableDB.java +++ b/ImageGallery/src/org/sleuthkit/autopsy/imagegallery/datamodel/DrawableDB.java @@ -47,7 +47,6 @@ import javax.annotation.Nonnull; import javax.annotation.concurrent.GuardedBy; import javax.swing.SortOrder; import org.apache.commons.lang3.StringUtils; -import org.openide.util.Exceptions; import org.sleuthkit.autopsy.casemodule.Case; import org.sleuthkit.autopsy.coreutils.Logger; import org.sleuthkit.autopsy.imagegallery.FileTypeUtils; @@ -102,6 +101,8 @@ public final class DrawableDB { private final PreparedStatement insertHashHitStmt; + private final PreparedStatement insertDataSourceStmt; + private final PreparedStatement updateFileStmt; private final PreparedStatement insertFileStmt; @@ -208,6 +209,10 @@ public final class DrawableDB { "INSERT OR IGNORE INTO drawable_files (obj_id , path, name, created_time, modified_time, make, model, analyzed) " //NON-NLS + "VALUES (?,?,?,?,?,?,?,?)"); //NON-NLS + insertDataSourceStmt = prepareStatement( + "INSERT OR IGNORE INTO datasources (ds_obj_id) " //NON-NLS + + "VALUES (?)"); //NON-NLS + removeFileStmt = prepareStatement("DELETE FROM drawable_files WHERE obj_id = ?"); //NON-NLS pathGroupStmt = prepareStatement("SELECT obj_id , analyzed FROM drawable_files WHERE path = ? ", DrawableAttribute.PATH); //NON-NLS @@ -348,6 +353,17 @@ public final class DrawableDB { LOGGER.log(Level.SEVERE, "problem accessing database", ex); //NON-NLS return false; } + + try (Statement stmt = con.createStatement()) { + String sql = "CREATE TABLE if not exists datasources " //NON-NLS + + "( id INTEGER PRIMARY KEY, " //NON-NLS + + " ds_obj_id integer UNIQUE NOT NULL)"; //NON-NLS + stmt.execute(sql); + } catch (SQLException ex) { + LOGGER.log(Level.SEVERE, "problem creating datasources table", ex); //NON-NLS + return false; + } + try (Statement stmt = con.createStatement()) { String sql = "CREATE TABLE if not exists drawable_files " //NON-NLS + "( obj_id INTEGER PRIMARY KEY, " //NON-NLS @@ -688,6 +704,67 @@ public final class DrawableDB { } } + + /** + * Gets all data source object ids from datasources table + * + * @return list of known data source object ids + */ + public Set getDataSourceIds() throws TskCoreException { + Statement statement = null; + ResultSet rs = null; + Set ret = new HashSet<>(); + dbReadLock(); + try { + statement = con.createStatement(); + rs = statement.executeQuery("SELECT ds_obj_id FROM datasources "); //NON-NLS + while (rs.next()) { + ret.add(rs.getLong(1)); + } + } catch (SQLException e) { + throw new TskCoreException("SQLException while getting data source object ids", e); + } finally { + if (rs != null) { + try { + rs.close(); + } catch (SQLException ex) { + LOGGER.log(Level.SEVERE, "Error closing resultset", ex); //NON-NLS + } + } + if (statement != null) { + try { + statement.close(); + } catch (SQLException ex) { + LOGGER.log(Level.SEVERE, "Error closing statement ", ex); //NON-NLS + } + } + dbReadUnlock(); + } + return ret; + } + + + /** + * Insert given data source object id into datasources table + * + * If the object id exists in the table already, it does nothing. + * + * @param dsObjectId data source object id to insert + */ + public void insertDataSource(long dsObjectId) { + dbWriteLock(); + try { + // "INSERT OR IGNORE/ INTO datasources (ds_obj_id)" + insertDataSourceStmt.setLong(1,dsObjectId); + + insertDataSourceStmt.executeUpdate(); + } catch (SQLException | NullPointerException ex) { + LOGGER.log(Level.SEVERE, "failed to insert/update datasources table", ex); //NON-NLS + } finally { + dbWriteUnlock(); + } + } + public DrawableTransaction beginTransaction() { return new DrawableTransaction(); }