Merge branch 'GP-4433_ghidraffe_GhidraGo_accept_DomainFolder_GhidraURL'

This commit is contained in:
ghidra1 2024-05-02 20:04:00 -04:00
commit da8ff58ba8
12 changed files with 399 additions and 63 deletions

View file

@ -22,23 +22,26 @@ import ghidra.app.CorePluginPackage;
import ghidra.app.plugin.PluginCategoryNames;
import ghidra.app.plugin.core.go.ipc.GhidraGoListener;
import ghidra.framework.main.*;
import ghidra.framework.model.ToolServices;
import ghidra.framework.plugintool.*;
import ghidra.framework.plugintool.util.PluginStatus;
import ghidra.framework.protocol.ghidra.GhidraURL;
import ghidra.util.Msg;
import ghidra.util.Swing;
//@formatter:off
@PluginInfo(
category = PluginCategoryNames.COMMON,
status = PluginStatus.UNSTABLE,
packageName = CorePluginPackage.NAME,
shortDescription = "Listens for new GhidraURL's to launch using ToolServices",
description = "Polls the ghidraGo directory for any url files written by the GhidraGoClient and " +
"processes them in Ghidra",
shortDescription = "Listens for new GhidraURL's to launch using FrontEndTool's" +
" accept method",
description = "Polls the ghidraGo directory for any url files written by the " +
"GhidraGoSender and processes them in Ghidra",
eventsConsumed = {ProjectPluginEvent.class})
//@formatter:on
/**
* Polls the ghidraGo directory located in the user's temporary directory for any url files written
* by the {@link GhidraGoSender} and processes them in Ghidra.
*/
public class GhidraGoPlugin extends Plugin implements ApplicationLevelOnlyPlugin {
private GhidraGoListener listener;
@ -69,7 +72,7 @@ public class GhidraGoPlugin extends Plugin implements ApplicationLevelOnlyPlugin
else {
try {
listener = new GhidraGoListener((url) -> {
processGhidraURL(url);
accept(url);
});
}
catch (IOException e) {
@ -81,26 +84,14 @@ public class GhidraGoPlugin extends Plugin implements ApplicationLevelOnlyPlugin
}
/**
* If the active project is null, do nothing.
* Otherwise, try and open the url using {@link ToolServices} launchDefaultToolWithURL function.
* @param ghidraURL the GhidraURL to open.
* Accept the given url, which is then passed to the FrontEndTool to process.
* @param url a {@link GhidraURL}
* @return true if handled successfully, false otherwise.
*/
private void processGhidraURL(URL ghidraURL) {
Msg.info(this, "GhidraGo processing " + ghidraURL);
try {
Msg.info(this,
"Accepting the resource at " + GhidraURL.getProjectURL(ghidraURL));
Swing.runNow(() -> {
FrontEndTool frontEnd = AppInfo.getFrontEndTool();
frontEnd.toFront();
frontEnd.getToolServices().launchDefaultToolWithURL(ghidraURL);
});
}
catch (IllegalArgumentException e) {
Msg.showError(this, null, "GhidraGo Unable to process GhidraURL",
"GhidraGo could not process " + ghidraURL, e);
}
public boolean accept(URL url) {
Msg.info(this, "GhidraGo accepting the resource at " + GhidraURL.getProjectURL(url));
FrontEndTool frontEndTool = AppInfo.getFrontEndTool();
frontEndTool.toFront();
return frontEndTool.accept(url);
}
}

View file

@ -29,8 +29,7 @@ import ghidra.GhidraApplicationLayout;
import ghidra.GhidraGo;
import ghidra.app.plugin.core.go.ipc.CheckForFileProcessedRunnable;
import ghidra.app.plugin.core.go.ipc.CheckForListenerRunnable;
import ghidra.framework.model.DomainFile;
import ghidra.framework.model.DomainFolder;
import ghidra.framework.model.*;
import ghidra.framework.plugintool.PluginTool;
import ghidra.framework.protocol.ghidra.GhidraURL;
import ghidra.program.model.listing.Program;
@ -40,33 +39,40 @@ import ghidra.util.task.TaskMonitor;
public class GhidraGoPluginTest extends AbstractGhidraHeadedIntegrationTest {
private final static String DIRECTORY_NAME = getTestDirectoryPath();
private final static String ACTIVE_PROJECT = "active";
private final static String INACTIVE_PROJECT = "inactive";
private TestEnv env;
private PluginTool tool;
private GhidraGo ghidraGo;
private URL url;
private Project inactiveProject;
private GhidraApplicationLayout layout;
@Before
public void setUp() throws Exception {
env = new TestEnv();
// clean up projects
ProjectTestUtils.deleteProject(DIRECTORY_NAME, ACTIVE_PROJECT);
ProjectTestUtils.deleteProject(DIRECTORY_NAME, INACTIVE_PROJECT);
// create inactive project if it doesn't exist
ProjectTestUtils.getProject(DIRECTORY_NAME, INACTIVE_PROJECT).close();
// add program and folder to inactive project
inactiveProject = ProjectTestUtils.getProject(DIRECTORY_NAME, INACTIVE_PROJECT);
addProgramAndFolderToProject(inactiveProject);
inactiveProject.close();
// set up test env and add GhidraGoPlugin to the front end tool.
env = new TestEnv(ACTIVE_PROJECT);
tool = env.getFrontEndTool();
tool.addPlugin(GhidraGoPlugin.class.getName());
showTool(tool);
DomainFolder rootFolder = env.getProject().getProjectData().getRootFolder();
Program p = createNotepadProgram();
rootFolder.createFile("notepad", p, TaskMonitor.DUMMY);
env.release(p);
url = GhidraURL.makeURL(env.getProjectManager().getActiveProject().getProjectLocator(),
"/notepad", null);
layout = (GhidraApplicationLayout) createApplicationLayout();
addProgramAndFolderToProject(env.getProject());
// initialize GhidraGo client
ghidraGo = new GhidraGo();
CheckForFileProcessedRunnable.WAIT_FOR_PROCESSING_DELAY_MS = 1000;
@ -78,6 +84,14 @@ public class GhidraGoPluginTest extends AbstractGhidraHeadedIntegrationTest {
CheckForListenerRunnable.WAIT_FOR_LISTENER_PERIOD_MS = 10;
}
private void addProgramAndFolderToProject(Project p) throws Exception {
Program program = createNotepadProgram();
DomainFolder rootFolder = p.getProjectData().getRootFolder();
rootFolder.createFile("notepad", program, TaskMonitor.DUMMY);
rootFolder.createFolder("testFolder");
}
private Program createNotepadProgram() throws Exception {
ClassicSampleX86ProgramBuilder builder =
new ClassicSampleX86ProgramBuilder("notepad", false, this);
@ -87,11 +101,18 @@ public class GhidraGoPluginTest extends AbstractGhidraHeadedIntegrationTest {
@After
public void tearDown() {
ProjectTestUtils.deleteProject(DIRECTORY_NAME, ACTIVE_PROJECT);
ProjectTestUtils.deleteProject(DIRECTORY_NAME, INACTIVE_PROJECT);
env.dispose();
}
@Test
public void testProcessingUrl() throws Exception {
public void testLaunchingWithProgramUrl() throws Exception {
// given a valid local GhidraURL pointing to a program
URL url = GhidraURL.makeURL(env.getProjectManager().getActiveProject().getProjectLocator(),
"/notepad", null);
// when ghidraGo is launched with the url
Swing.runLater(() -> {
try {
ghidraGo.launch(layout, new String[] { url.toString() });
@ -100,6 +121,8 @@ public class GhidraGoPluginTest extends AbstractGhidraHeadedIntegrationTest {
// empty
}
});
// then the code browser should be launched
waitForSwing();
waitFor(() -> Arrays.asList(tool.getToolServices().getRunningTools())
.stream()
@ -109,8 +132,9 @@ public class GhidraGoPluginTest extends AbstractGhidraHeadedIntegrationTest {
.stream()
.filter(p -> p.getName().equals("CodeBrowser"))
.findFirst();
assertTrue(cb.isPresent());
// and the domain file should be open in the code browser
assertTrue(Arrays.asList(cb.get().getDomainFiles())
.stream()
.map(DomainFile::getName)
@ -118,15 +142,114 @@ public class GhidraGoPluginTest extends AbstractGhidraHeadedIntegrationTest {
}
@Test
public void testLaunchingWithInvalidUrl() throws Exception {
public void testLaunchingWithProgramUrlForInactiveProject() throws Exception {
// given a valid local GhidraURL pointing to a program contained within the inactive project
URL url = GhidraURL.makeURL(inactiveProject.getProjectLocator(), "/notepad", null);
try {
// when ghidraGo is launched with the url
Swing.runLater(() -> {
try {
ghidraGo.launch(layout, new String[] { url.toString() });
}
catch (Exception e) {
// empty
}
});
// then the code browser should be launched
waitForSwing();
waitFor(() -> Arrays.asList(tool.getToolServices().getRunningTools())
.stream()
.map(PluginTool::getName)
.anyMatch(Predicate.isEqual("CodeBrowser")));
Optional<PluginTool> cb = Arrays.asList(tool.getToolServices().getRunningTools())
.stream()
.filter(p -> p.getName().equals("CodeBrowser"))
.findFirst();
assertTrue(cb.isPresent());
// and the domain file should be open in the code browser
assertTrue(Arrays.asList(cb.get().getDomainFiles())
.stream()
.map(DomainFile::getName)
.anyMatch(Predicate.isEqual("notepad")));
}
finally {
inactiveProject.close();
}
}
@Test
public void testLaunchingWithFolderUrl() throws Exception {
// given a valid local GhidraURL pointing to a folder within the active project
URL url = GhidraURL.makeURL(env.getProjectManager().getActiveProject().getProjectLocator(),
"/testFolder", null);
// when ghidraGo is launched with the url
Swing.runLater(() -> {
try {
ghidraGo.launch(layout, new String[] { "ghidra:/test" });
ghidraGo.launch(layout, new String[] { url.toString() });
}
catch (Exception e) {
// empty
}
});
// then the project window should select the folder within the active project data panel
waitForSwing();
ProjectLocator[] projViews = env.getProject().getProjectViews();
Assert.assertEquals(0, projViews.length);
}
@Test
public void testLaunchingWithFolderUrlForInactiveProject() throws Exception {
// given a valid local GhidraURL pointing to a folder within an in-active project
URL url =
GhidraURL.makeURL(inactiveProject.getProjectLocator(),
"/testFolder", null);
try {
// when ghidraGo is launched with the url
Swing.runLater(() -> {
try {
ghidraGo.launch(layout, new String[] { url.toString() });
}
catch (Exception e) {
// empty
}
});
// then the project window should select the folder within the viewed project data panel
waitForSwing();
ProjectLocator[] projViews = env.getProject().getProjectViews();
Assert.assertEquals(1, projViews.length);
}
finally {
inactiveProject.close();
}
}
@Test
public void testLaunchingWithResourceThatDoesNotExist() throws Exception {
// given a valid local GhidraURL pointing to a program that does not exist
URL url = GhidraURL.makeURL(env.getProjectManager().getActiveProject().getProjectLocator(),
"/test", null);
// when ghidraGo is launched with the url
Swing.runLater(() -> {
try {
ghidraGo.launch(layout, new String[] { url.toString() });
}
catch (Exception e) {
// empty
}
});
// then an error dialog should be displayed
AbstractErrDialog err = waitForErrorDialog();
assertEquals("Content Not Found", err.getTitle());
}

View file

@ -24,6 +24,7 @@ import ghidra.framework.main.AppInfo;
import ghidra.framework.model.*;
import ghidra.framework.store.FileSystem;
import ghidra.util.InvalidNameException;
import ghidra.util.Msg;
import ghidra.util.exception.CancelledException;
import ghidra.util.task.TaskMonitor;
@ -45,8 +46,7 @@ public class FolderLinkContentHandler extends LinkHandler<NullFolderDomainObject
if (!(obj instanceof URLLinkObject)) {
throw new IOException("Unsupported domain object: " + obj.getClass().getName());
}
return createFile((URLLinkObject) obj, FOLDER_LINK_CONTENT_TYPE, fs, path, name,
monitor);
return createFile((URLLinkObject) obj, FOLDER_LINK_CONTENT_TYPE, fs, path, name, monitor);
}
@Override
@ -91,6 +91,11 @@ public class FolderLinkContentHandler extends LinkHandler<NullFolderDomainObject
URL url = getURL(folderLinkFile);
Project activeProject = AppInfo.getActiveProject();
if (activeProject == null) {
Msg.error(FolderLinkContentHandler.class,
"Use of Linked Folders requires active project.");
return null;
}
GhidraFolder parent = ((GhidraFile) folderLinkFile).getParent();
return new LinkedGhidraFolder(activeProject, parent, folderLinkFile.getName(), url);
}

View file

@ -1067,10 +1067,14 @@ public class GhidraFileData {
if (folderItem.isCheckedOut() || versionedFolderItem != null) {
throw new IOException("File already versioned");
}
if (isLinkFile() && !GhidraURL.isServerRepositoryURL(LinkHandler.getURL(folderItem))) {
throw new IOException("Local project link-file may not be versioned");
ContentHandler<?> contentHandler = getContentHandler();
if (contentHandler instanceof LinkHandler linkHandler) {
// must check local vs remote URL
if (!GhidraURL.isServerRepositoryURL(LinkHandler.getURL(folderItem))) {
throw new IOException("Local project link-file may not be versioned");
}
}
if (getContentHandler().isPrivateContentType()) {
else if (contentHandler.isPrivateContentType()) {
throw new IOException("Content may not be versioned: " + getContentType());
}
}

View file

@ -165,8 +165,7 @@ public abstract class LinkHandler<T extends DomainObjectAdapterDB> extends DBCon
@Override
public final boolean isPrivateContentType() {
// NOTE: URL must be checked - only repository-based links may be versioned
return true;
throw new UnsupportedOperationException("Link file requires checking server vs local URL");
}
/**

View file

@ -34,8 +34,7 @@ public class LinkedGhidraFolder extends LinkedGhidraSubFolder {
public static Icon FOLDER_LINK_CLOSED_ICON =
new GIcon("icon.content.handler.linked.folder.closed");
public static Icon FOLDER_LINK_OPEN_ICON =
new GIcon("icon.content.handler.linked.folder.open");
public static Icon FOLDER_LINK_OPEN_ICON = new GIcon("icon.content.handler.linked.folder.open");
private final Project activeProject;
private final DomainFolder localParent;
@ -73,8 +72,8 @@ public class LinkedGhidraFolder extends LinkedGhidraSubFolder {
}
/**
* Get the Ghidra URL associated with this linked folder's project or repository
* @return Ghidra URL associated with this linked folder's project or repository
* Get the Ghidra URL of the project/repository folder referenced by this object
* @return Ghidra URL of the project/repository folder referenced by this object
*/
public URL getProjectURL() {
if (projectUrl == null) {
@ -83,6 +82,7 @@ public class LinkedGhidraFolder extends LinkedGhidraSubFolder {
return projectUrl;
}
@Override
LinkedGhidraFolder getLinkedRootFolder() {
return this;
}
@ -108,12 +108,12 @@ public class LinkedGhidraFolder extends LinkedGhidraSubFolder {
@Override
public ProjectLocator getProjectLocator() {
return activeProject.getProjectLocator();
return localParent.getProjectLocator();
}
@Override
public ProjectData getProjectData() {
return activeProject.getProjectData();
return localParent.getProjectData();
}
@Override

View file

@ -28,6 +28,10 @@ import ghidra.util.*;
import ghidra.util.exception.CancelledException;
import ghidra.util.task.TaskMonitor;
/**
* {@code LinkedGhidraSubFolder} corresponds to a {@link DomainFolder} contained within a
* {@link LinkedGhidraFolder} or another {@code LinkedGhidraSubFolder}.
*/
class LinkedGhidraSubFolder implements LinkedDomainFolder {
private final LinkedGhidraFolder linkedRootFolder;

View file

@ -0,0 +1,122 @@
/* ###
* IP: GHIDRA
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package ghidra.framework.main;
import java.io.File;
import java.io.IOException;
import java.net.URL;
import java.util.Set;
import ghidra.framework.data.FolderLinkContentHandler;
import ghidra.framework.model.*;
import ghidra.framework.protocol.ghidra.GhidraURLQueryTask;
import ghidra.util.Msg;
import ghidra.util.Swing;
import ghidra.util.task.TaskMonitor;
public class AcceptUrlContentTask extends GhidraURLQueryTask {
private FrontEndPlugin plugin;
public AcceptUrlContentTask(URL url, FrontEndPlugin plugin) {
super("Accepting URL", url);
this.plugin = plugin;
}
private boolean isSameLocalProject(ProjectLocator projectLoc1, ProjectLocator projectLoc2) {
if (projectLoc1.isTransient() || projectLoc2.isTransient()) {
return false;
}
if (!projectLoc1.getName().equals(projectLoc2.getName())) {
return false;
}
try {
File proj1Dir = projectLoc1.getProjectDir().getCanonicalFile();
File proj2Dir = projectLoc2.getProjectDir().getCanonicalFile();
return proj1Dir.equals(proj2Dir);
}
catch (IOException e) {
return false;
}
}
@Override
public void processResult(DomainFile domainFile, URL url, TaskMonitor monitor)
throws IOException {
Project activeProject = AppInfo.getActiveProject();
if (activeProject == null) {
Msg.showError(this, null, "Ghidra Error",
"Unable to accept URL without active project open");
return;
}
Swing.runNow(() -> {
if (FolderLinkContentHandler.FOLDER_LINK_CONTENT_TYPE
.equals(domainFile.getContentType())) {
// Simply select folder link-file within project - do not follow - let user do that.
if (isSameLocalProject(activeProject.getProjectLocator(),
domainFile.getProjectLocator())) {
// Select file within active project
DomainFile df =
activeProject.getProjectData().getFile(domainFile.getPathname());
if (df == null) {
return; // unexpected race condition
}
plugin.selectFiles(Set.of(df));
}
else {
// Select file within read-only viewed project
plugin.showInViewedProject(url, false);
}
}
else {
AppInfo.getFrontEndTool().getToolServices().launchDefaultToolWithURL(url);
}
});
}
@Override
public void processResult(DomainFolder domainFolder, URL url, TaskMonitor monitor)
throws IOException {
Project activeProject = AppInfo.getActiveProject();
if (activeProject == null) {
Msg.showError(this, null, "Ghidra Error",
"Unable to accept URL without active project open");
return;
}
Swing.runNow(() -> {
if (isSameLocalProject(activeProject.getProjectLocator(),
domainFolder.getProjectLocator())) {
// Select folder within active project
DomainFolder df =
activeProject.getProjectData().getFolder(domainFolder.getPathname());
if (df == null) {
return; // unexpected race condition
}
plugin.selectFolder(df);
}
else {
// Select folder within read-only viewed project
plugin.showInViewedProject(url, true);
}
});
}
}

View file

@ -51,6 +51,7 @@ import ghidra.framework.options.SaveState;
import ghidra.framework.plugintool.*;
import ghidra.framework.plugintool.util.PluginStatus;
import ghidra.framework.preferences.Preferences;
import ghidra.framework.protocol.ghidra.GhidraURL;
import ghidra.framework.remote.User;
import ghidra.util.*;
import ghidra.util.filechooser.GhidraFileChooserModel;
@ -539,8 +540,22 @@ public class FrontEndPlugin extends Plugin
SwingUtilities.invokeLater(() -> {
// there was a delete bug; make the set unmodifiable to catch this earlier
Set<DomainFile> unmodifiableFiles = Collections.unmodifiableSet(files);
if (dataTablePanel.isCapacityExceeded()) {
projectDataPanel.showTree();
}
else {
dataTablePanel.setSelectedDomainFiles(unmodifiableFiles);
}
dataTreePanel.selectDomainFiles(unmodifiableFiles);
dataTablePanel.setSelectedDomainFiles(unmodifiableFiles);
});
}
void selectFolder(final DomainFolder folder) {
// Do this later in case any of the given files are newly created, which means that the
// GUIs may have not yet been notified.
SwingUtilities.invokeLater(() -> {
projectDataPanel.showTree();
dataTreePanel.selectDomainFolder(folder);
});
}
@ -1081,7 +1096,7 @@ public class FrontEndPlugin extends Plugin
public void openDomainFile(DomainFile domainFile) {
if (FolderLinkContentHandler.FOLDER_LINK_CONTENT_TYPE.equals(domainFile.getContentType())) {
showLinkedFolder(domainFile);
showLinkedFolderInViewedProject(domainFile);
return;
}
@ -1109,7 +1124,7 @@ public class FrontEndPlugin extends Plugin
"opens this type of file");
}
private void showLinkedFolder(DomainFile domainFile) {
private void showLinkedFolderInViewedProject(DomainFile domainFile) {
try {
LinkedGhidraFolder linkedFolder =
@ -1123,7 +1138,12 @@ public class FrontEndPlugin extends Plugin
return;
}
DomainFolder domainFolder = linkedFolder.getLinkedFolder();
// Do not hang onto domainFile, linkedFolder or their underlying project data
ProjectData viewedProjectData = dtp.getProjectData();
DomainFolder domainFolder =
viewedProjectData.getFolder(linkedFolder.getLinkedPathname());
if (domainFolder != null) {
// delayed to ensure tree is displayed
Swing.runLater(() -> dtp.selectDomainFolder(domainFolder));
@ -1136,6 +1156,52 @@ public class FrontEndPlugin extends Plugin
}
void showInViewedProject(URL ghidraURL, boolean isFolder) {
ProjectDataTreePanel dtp = projectDataPanel.openView(GhidraURL.getProjectURL(ghidraURL));
if (dtp == null) {
return;
}
Swing.runLater(() -> {
// delayed to ensure tree is displayed
ProjectData viewedProjectData = dtp.getProjectData();
String path = GhidraURL.getProjectPathname(ghidraURL);
if (isFolder) {
DomainFolder viewedProjectFolder = getViewProjectFolder(viewedProjectData, path);
if (viewedProjectFolder != null) {
dtp.selectDomainFolder(viewedProjectFolder);
}
}
else {
DomainFile viewedProjectFile = getViewProjectFile(viewedProjectData, path);
if (viewedProjectFile != null) {
dtp.selectDomainFile(viewedProjectFile);
}
}
});
}
private DomainFile getViewProjectFile(ProjectData viewedProjectData, String path) {
if (path == null || path.endsWith(DomainFolder.SEPARATOR)) {
return null;
}
return viewedProjectData.getFile(path);
}
private DomainFolder getViewProjectFolder(ProjectData viewedProjectData, String path) {
if (path == null || path.equals(DomainFolder.SEPARATOR)) {
return viewedProjectData.getRootFolder();
}
if (path.endsWith(DomainFolder.SEPARATOR)) {
path = path.substring(0, path.length() - 1); // remove trailing separator
}
return viewedProjectData.getFolder(path);
}
private class MyToolChestChangeListener implements ToolChestChangeListener {
@Override

View file

@ -64,6 +64,7 @@ import ghidra.framework.plugintool.util.*;
import ghidra.framework.preferences.Preferences;
import ghidra.framework.project.tool.GhidraTool;
import ghidra.framework.project.tool.GhidraToolTemplate;
import ghidra.framework.protocol.ghidra.GhidraURL;
import ghidra.util.*;
import ghidra.util.bean.GGlassPane;
import ghidra.util.classfinder.ClassSearcher;
@ -176,6 +177,15 @@ public class FrontEndTool extends PluginTool implements OptionsChangeListener {
System.exit(0);
}
@Override
public boolean accept(URL url) {
if (!GhidraURL.isLocalProjectURL(url) && !GhidraURL.isServerRepositoryURL(url)) {
return false;
}
Swing.runLater(() -> execute(new AcceptUrlContentTask(url, plugin)));
return true;
}
private void ensureSize() {
JFrame frame = getToolFrame();
Dimension size = frame.getSize();

View file

@ -406,6 +406,10 @@ class ProjectDataPanel extends JSplitPane implements ProjectViewListener {
}
}
void showTree() {
projectTab.setSelectedIndex(0);
}
private void showTable() {
projectTab.setSelectedIndex(1);
}

View file

@ -103,6 +103,14 @@ public class ProjectDataTablePanel extends JPanel {
new ProjectDataTableDnDHandler(gTable, model);
}
/**
* Determine if table capacity has been exceeded and files are not shown
* @return true if files are not shown in project data table, else false
*/
public boolean isCapacityExceeded() {
return capacityExceeded;
}
public void dispose() {
table.dispose(); // this will dispose the gTable as well
}