From 113a15b0b7e865001d11a3ca04a963fdcbc79574 Mon Sep 17 00:00:00 2001 From: APriestman Date: Tue, 5 May 2015 13:13:28 -0400 Subject: [PATCH 1/2] Various Image Gallery performance improvements. - Keep some file data in memory to avoid excessive database queries (list of image file IDs, image/video, and category counts) - When categorizing/tagging a group, don't create tiles (including thumbnails) for images that aren't on the screen - Changed locking around Category and Tag listeners to prevent slowdown/deadlocks --- .../imagegallery/ImageGalleryController.java | 15 +- .../actions/AddDrawableTagAction.java | 4 + .../imagegallery/actions/AddTagAction.java | 7 + .../actions/CategorizeAction.java | 8 +- .../imagegallery/datamodel/Category.java | 9 +- .../imagegallery/datamodel/DrawableDB.java | 153 +++++++++++++++++- .../imagegallery/datamodel/DrawableFile.java | 11 ++ .../imagegallery/grouping/GroupManager.java | 38 +---- .../autopsy/imagegallery/gui/GroupPane.java | 10 +- 9 files changed, 201 insertions(+), 54 deletions(-) diff --git a/ImageGallery/src/org/sleuthkit/autopsy/imagegallery/ImageGalleryController.java b/ImageGallery/src/org/sleuthkit/autopsy/imagegallery/ImageGalleryController.java index ac6b844c3e..1614de59b3 100644 --- a/ImageGallery/src/org/sleuthkit/autopsy/imagegallery/ImageGalleryController.java +++ b/ImageGallery/src/org/sleuthkit/autopsy/imagegallery/ImageGalleryController.java @@ -155,6 +155,10 @@ public final class ImageGalleryController { public GroupManager getGroupManager() { return groupManager; } + + public DrawableDB getDatabase(){ + return db; + } public void setListeningEnabled(boolean enabled) { listeningEnabled.set(enabled); @@ -338,6 +342,7 @@ public final class ImageGalleryController { restartWorker(); historyManager.clear(); groupManager.setDB(db); + db.initializeImageList(); SummaryTablePane.getDefault().handleCategoryChanged(Collections.emptyList()); } @@ -623,9 +628,9 @@ public final class ImageGalleryController { @Override public void run() { try{ - DrawableFile drawableFile = DrawableFile.create(getFile(), true); + DrawableFile drawableFile = DrawableFile.create(getFile(), true, db.isVideoFile(getFile())); db.updateFile(drawableFile); - } catch (NullPointerException ex){ + } catch (NullPointerException | TskCoreException ex){ // This is one of the places where we get many errors if the case is closed during processing. // We don't want to print out a ton of exceptions if this is the case. if(Case.isCaseOpen()){ @@ -710,14 +715,14 @@ public final class ImageGalleryController { if (hasMimeType == null) { if (ImageGalleryModule.isSupported(f)) { //no mime type but supported => add as not analyzed - db.insertFile(DrawableFile.create(f, false), tr); + db.insertFile(DrawableFile.create(f, false, db.isVideoFile(f)), tr); } else { //no mime type, not supported => remove ( should never get here) db.removeFile(f.getId(), tr); } } else { if (hasMimeType) { // supported mimetype => analyzed - db.updateFile(DrawableFile.create(f, true), tr); + db.updateFile(DrawableFile.create(f, true, db.isVideoFile(f)), tr); } else { //unsupported mimtype => analyzed but shouldn't include db.removeFile(f.getId(), tr); } @@ -828,7 +833,7 @@ public final class ImageGalleryController { progressHandle.finish(); break; } - db.insertFile(DrawableFile.create(f, false), tr); + db.insertFile(DrawableFile.create(f, false, db.isVideoFile(f)), tr); units++; final int prog = units; progressHandle.progress(f.getName(), units); diff --git a/ImageGallery/src/org/sleuthkit/autopsy/imagegallery/actions/AddDrawableTagAction.java b/ImageGallery/src/org/sleuthkit/autopsy/imagegallery/actions/AddDrawableTagAction.java index 261b755284..a098e1a1c9 100644 --- a/ImageGallery/src/org/sleuthkit/autopsy/imagegallery/actions/AddDrawableTagAction.java +++ b/ImageGallery/src/org/sleuthkit/autopsy/imagegallery/actions/AddDrawableTagAction.java @@ -75,7 +75,11 @@ public class AddDrawableTagAction extends AddTagAction { @Override public void addTag(TagName tagName, String comment) { Set selectedFiles = new HashSet<>(FileIDSelectionModel.getInstance().getSelected()); + addTagsToFiles(tagName, comment, selectedFiles); + } + @Override + public void addTagsToFiles(TagName tagName, String comment, Set selectedFiles){ new SwingWorker() { @Override diff --git a/ImageGallery/src/org/sleuthkit/autopsy/imagegallery/actions/AddTagAction.java b/ImageGallery/src/org/sleuthkit/autopsy/imagegallery/actions/AddTagAction.java index 40e59fd44d..aef2ef061c 100644 --- a/ImageGallery/src/org/sleuthkit/autopsy/imagegallery/actions/AddTagAction.java +++ b/ImageGallery/src/org/sleuthkit/autopsy/imagegallery/actions/AddTagAction.java @@ -21,6 +21,7 @@ package org.sleuthkit.autopsy.imagegallery.actions; import org.sleuthkit.autopsy.imagegallery.datamodel.Category; import java.lang.reflect.InvocationTargetException; import java.util.List; +import java.util.Set; import java.util.logging.Level; import javafx.event.ActionEvent; import javafx.scene.control.Menu; @@ -65,6 +66,12 @@ abstract class AddTagAction { * comment to one or more a SleuthKit data model objects. */ abstract protected void addTag(TagName tagName, String comment); + + /** + * Template method to allow derived classes to add the indicated tag and + * comment to a list of one or more file IDs. + */ + abstract protected void addTagsToFiles(TagName tagName, String comment, Set selectedFiles); /** * Instances of this class implement a context menu user interface for diff --git a/ImageGallery/src/org/sleuthkit/autopsy/imagegallery/actions/CategorizeAction.java b/ImageGallery/src/org/sleuthkit/autopsy/imagegallery/actions/CategorizeAction.java index cf1d497a85..548126e418 100644 --- a/ImageGallery/src/org/sleuthkit/autopsy/imagegallery/actions/CategorizeAction.java +++ b/ImageGallery/src/org/sleuthkit/autopsy/imagegallery/actions/CategorizeAction.java @@ -72,7 +72,11 @@ public class CategorizeAction extends AddTagAction { @Override public void addTag(TagName tagName, String comment) { Set selectedFiles = new HashSet<>(FileIDSelectionModel.getInstance().getSelected()); - + addTagsToFiles(tagName, comment, selectedFiles); + } + + @Override + public void addTagsToFiles(TagName tagName, String comment, Set selectedFiles){ //TODO: should this get submitted to controller rather than a swingworker ? -jm new SwingWorker() { @@ -97,9 +101,11 @@ public class CategorizeAction extends AddTagAction { if (ct.getName().getDisplayName().startsWith(Category.CATEGORY_PREFIX)) { LOGGER.log(Level.INFO, "removing old category from {0}", file.getName()); Case.getCurrentCase().getServices().getTagsManager().deleteContentTag(ct); + controller.getDatabase().decrementCategoryCount(Category.fromDisplayName(ct.getName().getDisplayName())); } } + controller.getDatabase().incrementCategoryCount(Category.fromDisplayName(tagName.getDisplayName())); if (tagName != Category.ZERO.getTagName()) { // no tags for cat-0 Case.getCurrentCase().getServices().getTagsManager().addContentTag(file, tagName, comment); } diff --git a/ImageGallery/src/org/sleuthkit/autopsy/imagegallery/datamodel/Category.java b/ImageGallery/src/org/sleuthkit/autopsy/imagegallery/datamodel/Category.java index 25627626d3..7a45f62f49 100644 --- a/ImageGallery/src/org/sleuthkit/autopsy/imagegallery/datamodel/Category.java +++ b/ImageGallery/src/org/sleuthkit/autopsy/imagegallery/datamodel/Category.java @@ -66,11 +66,14 @@ public enum Category implements Comparable { private final static Set listeners = new HashSet<>(); public static void fireChange(Collection ids) { + Set listenersCopy = new HashSet(listeners); synchronized (listeners) { - for (CategoryListener list : listeners) { - list.handleCategoryChanged(ids); - } + listenersCopy.addAll(listeners); } + for (CategoryListener list : listenersCopy) { + list.handleCategoryChanged(ids); + } + } public static void registerListener(CategoryListener aThis) { diff --git a/ImageGallery/src/org/sleuthkit/autopsy/imagegallery/datamodel/DrawableDB.java b/ImageGallery/src/org/sleuthkit/autopsy/imagegallery/datamodel/DrawableDB.java index 4d822eae7a..16b1b63891 100644 --- a/ImageGallery/src/org/sleuthkit/autopsy/imagegallery/datamodel/DrawableDB.java +++ b/ImageGallery/src/org/sleuthkit/autopsy/imagegallery/datamodel/DrawableDB.java @@ -36,6 +36,7 @@ import java.util.Set; import java.util.concurrent.locks.Lock; import java.util.concurrent.locks.ReentrantReadWriteLock; import java.util.logging.Level; +import javax.annotation.concurrent.GuardedBy; import javax.swing.SortOrder; import org.apache.commons.lang.StringUtils; import org.openide.util.Exceptions; @@ -43,6 +44,7 @@ import org.sleuthkit.autopsy.casemodule.Case; import org.sleuthkit.autopsy.coreutils.Logger; import org.sleuthkit.autopsy.imagegallery.FileUpdateEvent; import org.sleuthkit.autopsy.imagegallery.ImageGalleryController; +import org.sleuthkit.autopsy.imagegallery.ImageGalleryModule; import org.sleuthkit.autopsy.imagegallery.grouping.GroupKey; import org.sleuthkit.autopsy.imagegallery.grouping.GroupManager; import org.sleuthkit.autopsy.imagegallery.grouping.GroupSortBy; @@ -555,9 +557,12 @@ public class DrawableDB { if (tr.isClosed()) { throw new IllegalArgumentException("can't update database with closed transaction"); } + dbWriteLock(); try { - + // Update the list of file IDs in memory + addImageFileToList(f.getId()); + // "INSERT OR IGNORE/ INTO drawable_files (path, name, created_time, modified_time, make, model, analyzed)" stmt.setLong(1, f.getId()); stmt.setString(2, f.getDrawablePath()); @@ -939,7 +944,8 @@ public class DrawableDB { */ private DrawableFile getFileFromID(Long id, boolean analyzed) throws TskCoreException { try { - return DrawableFile.create(controller.getSleuthKitCase().getAbstractFileById(id), analyzed); + AbstractFile f = controller.getSleuthKitCase().getAbstractFileById(id); + return DrawableFile.create(f, analyzed, isVideoFile(f)); } catch (IllegalStateException ex) { LOGGER.log(Level.SEVERE, "there is no case open; failed to load file with id: " + id, ex); return null; @@ -956,8 +962,9 @@ public class DrawableDB { */ public DrawableFile getFileFromID(Long id) throws TskCoreException { try { - return DrawableFile.create(controller.getSleuthKitCase().getAbstractFileById(id), - areFilesAnalyzed(Collections.singleton(id))); + AbstractFile f = controller.getSleuthKitCase().getAbstractFileById(id); + return DrawableFile.create(f, + areFilesAnalyzed(Collections.singleton(id)), isVideoFile(f)); } catch (IllegalStateException ex) { LOGGER.log(Level.SEVERE, "there is no case open; failed to load file with id: " + id); return null; @@ -1038,7 +1045,8 @@ public class DrawableDB { List contentTags = Case.getCurrentCase().getServices().getTagsManager().getContentTagsByTagName(cat.getTagName()); for (ContentTag ct : contentTags) { if (ct.getContent() instanceof AbstractFile) { - files.add(DrawableFile.create((AbstractFile) ct.getContent(), isFileAnalyzed(ct.getContent().getId()))); + files.add(DrawableFile.create((AbstractFile) ct.getContent(), isFileAnalyzed(ct.getContent().getId()), + isVideoFile((AbstractFile) ct.getContent()))); } } return files; @@ -1085,6 +1093,9 @@ public class DrawableDB { dbWriteLock(); try { + // Update the list of file IDs in memory + removeImageFileFromList(id); + //"delete from drawable_files where (obj_id = " + id + ")" removeFileStmt.setLong(1, id); removeFileStmt.executeUpdate(); @@ -1107,6 +1118,138 @@ public class DrawableDB { super(CANNOT_HAVE_MORE_THAN_ONE_OPEN_TRANSACTIO); } } + + /** + * For performance reasons, keep a list of all file IDs currently in the image database. + * Otherwise the database is queried many times to retrieve the same data. + */ + @GuardedBy("fileIDlist") + private final Set fileIDlist = new HashSet<>(); + + public boolean isImageFile(Long id) { + synchronized (fileIDlist) { + return fileIDlist.contains(id); + } + } + + public void addImageFileToList(Long id) { + synchronized (fileIDlist) { + fileIDlist.add(id); + } + } + + public void removeImageFileFromList(Long id) { + synchronized (fileIDlist) { + fileIDlist.remove(id); + } + } + + public void initializeImageList(){ + synchronized (fileIDlist){ + dbReadLock(); + try { + Statement stmt = con.createStatement(); + ResultSet analyzedQuery = stmt.executeQuery("select obj_id from drawable_files"); + while (analyzedQuery.next()) { + addImageFileToList(analyzedQuery.getLong(OBJ_ID)); + } + + } catch (SQLException ex) { + LOGGER.log(Level.WARNING, "problem loading file IDs: ", ex); + } finally { + dbReadUnlock(); + } + } + } + + /** + * For performance reasons, keep current category counts in memory + */ + @GuardedBy("categoryCounts") + private final Map categoryCounts = new HashMap<>(); + + public void incrementCategoryCount(Category cat) throws TskCoreException{ + synchronized(categoryCounts){ + int count = getCategoryCount(cat); + count++; + categoryCounts.put(cat, count); + } + } + + public void decrementCategoryCount(Category cat) throws TskCoreException{ + synchronized(categoryCounts){ + int count = getCategoryCount(cat); + count--; + categoryCounts.put(cat, count); + } + } + + public int getCategoryCount(Category cat) throws TskCoreException{ + synchronized(categoryCounts){ + if(categoryCounts.containsKey(cat)){ + return categoryCounts.get(cat); + } + else{ + try { + if (cat == Category.ZERO) { + + // Category Zero (Uncategorized) files will not be tagged as such - + // this is really just the default setting. So we count the number of files + // tagged with the other categories and subtract from the total. + int allOtherCatCount = 0; + TagName[] tns = {Category.FOUR.getTagName(), Category.THREE.getTagName(), Category.TWO.getTagName(), Category.ONE.getTagName(), Category.FIVE.getTagName()}; + for (TagName tn : tns) { + List contentTags = Case.getCurrentCase().getServices().getTagsManager().getContentTagsByTagName(tn); + for (ContentTag ct : contentTags) { + if(ct.getContent() instanceof AbstractFile){ + AbstractFile f = (AbstractFile)ct.getContent(); + if(this.isImageFile(f.getId())){ + allOtherCatCount++; + } + } + } + } + categoryCounts.put(cat, this.countAllFiles() - allOtherCatCount); + return categoryCounts.get(cat); + } else { + + int fileCount = 0; + List contentTags = Case.getCurrentCase().getServices().getTagsManager().getContentTagsByTagName(cat.getTagName()); + for (ContentTag ct : contentTags) { + if(ct.getContent() instanceof AbstractFile){ + AbstractFile f = (AbstractFile)ct.getContent(); + if(this.isImageFile(f.getId())){ + fileCount++; + } + } + } + categoryCounts.put(cat, fileCount); + return fileCount; + } + } catch(IllegalStateException ex){ + throw new TskCoreException("Case closed while getting files"); + } + } + } + } + + /** + * For performance reasons, keep the file type in memory + */ + @GuardedBy("videoFileMap") + private final Map videoFileMap = new HashMap<>(); + + public boolean isVideoFile(AbstractFile f) throws TskCoreException{ + synchronized(videoFileMap){ + if(videoFileMap.containsKey(f.getId())){ + return videoFileMap.get(f.getId()); + } + + boolean isVideo = ImageGalleryModule.isVideoFile(f); + videoFileMap.put(f.getId(), isVideo); + return isVideo; + } + } /** * inner class that can reference access database connection diff --git a/ImageGallery/src/org/sleuthkit/autopsy/imagegallery/datamodel/DrawableFile.java b/ImageGallery/src/org/sleuthkit/autopsy/imagegallery/datamodel/DrawableFile.java index a06fcd226c..886f70e9a7 100644 --- a/ImageGallery/src/org/sleuthkit/autopsy/imagegallery/datamodel/DrawableFile.java +++ b/ImageGallery/src/org/sleuthkit/autopsy/imagegallery/datamodel/DrawableFile.java @@ -67,6 +67,17 @@ public abstract class DrawableFile extends AbstractFile return new ImageFile<>(abstractFileById, analyzed); } } + + /** + * Skip the database query if we have already determined the file type. + */ + public static DrawableFile create(AbstractFile abstractFileById, boolean analyzed, boolean isVideo) { + if (isVideo) { + return new VideoFile<>(abstractFileById, analyzed); + } else { + return new ImageFile<>(abstractFileById, analyzed); + } + } public static DrawableFile create(Long id, boolean analyzed) throws TskCoreException, IllegalStateException { diff --git a/ImageGallery/src/org/sleuthkit/autopsy/imagegallery/grouping/GroupManager.java b/ImageGallery/src/org/sleuthkit/autopsy/imagegallery/grouping/GroupManager.java index 4a68c678e3..1e334c9d67 100644 --- a/ImageGallery/src/org/sleuthkit/autopsy/imagegallery/grouping/GroupManager.java +++ b/ImageGallery/src/org/sleuthkit/autopsy/imagegallery/grouping/GroupManager.java @@ -531,42 +531,7 @@ public class GroupManager implements FileUpdateEvent.FileUpdateListener { * @throws TskCoreException */ public int countFilesWithCategory(Category category) throws TskCoreException { - try { - if (category == Category.ZERO) { - - // It is unlikely that Category Zero (Uncategorized) files will be tagged as such - - // this is really just the default setting. So we count the number of files - // tagged with the other categories and subtract from the total. - int allOtherCatCount = 0; - TagName[] tns = {Category.FOUR.getTagName(), Category.THREE.getTagName(), Category.TWO.getTagName(), Category.ONE.getTagName(), Category.FIVE.getTagName()}; - for (TagName tn : tns) { - List contentTags = Case.getCurrentCase().getServices().getTagsManager().getContentTagsByTagName(tn); - for (ContentTag ct : contentTags) { - if (ct.getContent() instanceof AbstractFile && ImageGalleryModule.isSupportedAndNotKnown((AbstractFile) ct.getContent())) { - allOtherCatCount++; - } - } - } - return (db.countAllFiles() - allOtherCatCount); - } else { - - int fileCount = 0; - List contentTags = Case.getCurrentCase().getServices().getTagsManager().getContentTagsByTagName(category.getTagName()); - for (ContentTag ct : contentTags) { - if (ct.getContent() instanceof AbstractFile && ImageGalleryModule.isSupportedAndNotKnown((AbstractFile) ct.getContent())) { - fileCount++; - } - } - - return fileCount; - } - } catch (TskCoreException ex) { - LOGGER.log(Level.WARNING, "TSK error getting files in Category:" + category.getDisplayName(), ex); - throw ex; - } catch(IllegalStateException ex){ - throw new TskCoreException("Case closed while getting files"); - } - + return db.getCategoryCount(category); } public List getFileIDsWithTag(TagName tagName) throws TskCoreException { @@ -738,6 +703,7 @@ public class GroupManager implements FileUpdateEvent.FileUpdateListener { TagUtils.fireChange(fileIDs); } break; + } } diff --git a/ImageGallery/src/org/sleuthkit/autopsy/imagegallery/gui/GroupPane.java b/ImageGallery/src/org/sleuthkit/autopsy/imagegallery/gui/GroupPane.java index 8d9daf1482..43374e6cb6 100644 --- a/ImageGallery/src/org/sleuthkit/autopsy/imagegallery/gui/GroupPane.java +++ b/ImageGallery/src/org/sleuthkit/autopsy/imagegallery/gui/GroupPane.java @@ -22,6 +22,8 @@ import com.google.common.collect.ImmutableMap; import java.net.URL; import java.util.ArrayList; import java.util.Arrays; +import java.util.Set; +import java.util.HashSet; import java.util.Collection; import java.util.HashMap; import java.util.List; @@ -277,8 +279,8 @@ public class GroupPane extends BorderPane implements GroupView { menuItem.setOnAction(new EventHandler() { @Override public void handle(ActionEvent t) { - selectAllFiles(); - new CategorizeAction().addTag(cat.getTagName(), ""); + Set fileIdSet = new HashSet(getGrouping().fileIds()); + new CategorizeAction().addTagsToFiles(cat.getTagName(), "", fileIdSet); grpCatSplitMenu.setText(cat.getDisplayName()); grpCatSplitMenu.setOnAction(this); @@ -292,8 +294,8 @@ public class GroupPane extends BorderPane implements GroupView { menuItem.setOnAction(new EventHandler() { @Override public void handle(ActionEvent t) { - selectAllFiles(); - AddDrawableTagAction.getInstance().addTag(tn, ""); + Set fileIdSet = new HashSet(getGrouping().fileIds()); + AddDrawableTagAction.getInstance().addTagsToFiles(tn, "", fileIdSet); grpTagSplitMenu.setText(tn.getDisplayName()); grpTagSplitMenu.setOnAction(this); From 0ac2bde11aae6c4af059a5208237e3fb84806acf Mon Sep 17 00:00:00 2001 From: APriestman Date: Wed, 6 May 2015 08:54:41 -0400 Subject: [PATCH 2/2] Clear tag data between cases. Changed lock on tag listener to prevent deadlocks. --- .../autopsy/imagegallery/ImageGalleryController.java | 1 + .../org/sleuthkit/autopsy/imagegallery/TagUtils.java | 10 +++++++--- .../autopsy/imagegallery/datamodel/Category.java | 12 ++++++++++++ 3 files changed, 20 insertions(+), 3 deletions(-) diff --git a/ImageGallery/src/org/sleuthkit/autopsy/imagegallery/ImageGalleryController.java b/ImageGallery/src/org/sleuthkit/autopsy/imagegallery/ImageGalleryController.java index 1614de59b3..1cab3d702c 100644 --- a/ImageGallery/src/org/sleuthkit/autopsy/imagegallery/ImageGalleryController.java +++ b/ImageGallery/src/org/sleuthkit/autopsy/imagegallery/ImageGalleryController.java @@ -357,6 +357,7 @@ public final class ImageGalleryController { Platform.runLater(() -> { historyManager.clear(); }); + Category.clearTagNames(); Toolbar.getDefault().reset(); groupManager.clear(); diff --git a/ImageGallery/src/org/sleuthkit/autopsy/imagegallery/TagUtils.java b/ImageGallery/src/org/sleuthkit/autopsy/imagegallery/TagUtils.java index f6e17c000c..b5eb4cfc25 100644 --- a/ImageGallery/src/org/sleuthkit/autopsy/imagegallery/TagUtils.java +++ b/ImageGallery/src/org/sleuthkit/autopsy/imagegallery/TagUtils.java @@ -20,7 +20,9 @@ package org.sleuthkit.autopsy.imagegallery; import java.util.ArrayList; import java.util.Collection; +import java.util.HashSet; import java.util.List; +import java.util.Set; import java.util.logging.Level; import javafx.event.ActionEvent; import javafx.event.EventHandler; @@ -92,10 +94,12 @@ public class TagUtils { } public static void fireChange(Collection ids) { + Set listenersCopy = new HashSet(listeners); synchronized (listeners) { - for (TagListener list : listeners) { - list.handleTagsChanged(ids); - } + listenersCopy.addAll(listeners); + } + for (TagListener list : listenersCopy) { + list.handleTagsChanged(ids); } } diff --git a/ImageGallery/src/org/sleuthkit/autopsy/imagegallery/datamodel/Category.java b/ImageGallery/src/org/sleuthkit/autopsy/imagegallery/datamodel/Category.java index 7a45f62f49..dcf0862875 100644 --- a/ImageGallery/src/org/sleuthkit/autopsy/imagegallery/datamodel/Category.java +++ b/ImageGallery/src/org/sleuthkit/autopsy/imagegallery/datamodel/Category.java @@ -153,6 +153,18 @@ public enum Category implements Comparable { }); return menuItem; } + + /** + * Use when closing a case to make sure everything is re-initialized in the next case. + */ + public static void clearTagNames(){ + Category.ZERO.tagName = null; + Category.ONE.tagName = null; + Category.TWO.tagName = null; + Category.THREE.tagName = null; + Category.FOUR.tagName = null; + Category.FIVE.tagName = null; + } public static interface CategoryListener {