diff --git a/Core/manifest.mf b/Core/manifest.mf index a671f42879..049f93c861 100644 --- a/Core/manifest.mf +++ b/Core/manifest.mf @@ -2,7 +2,7 @@ Manifest-Version: 1.0 OpenIDE-Module: org.sleuthkit.autopsy.core/7 OpenIDE-Module-Localizing-Bundle: org/sleuthkit/autopsy/core/Bundle.properties OpenIDE-Module-Layer: org/sleuthkit/autopsy/core/layer.xml -OpenIDE-Module-Implementation-Version: 6 +OpenIDE-Module-Implementation-Version: 7 OpenIDE-Module-Requires: org.openide.windows.WindowManager, org.netbeans.api.javahelp.Help AutoUpdate-Show-In-Client: true AutoUpdate-Essential-Module: true diff --git a/Core/nbproject/project.properties b/Core/nbproject/project.properties index 73d8710d32..fcae93871e 100644 --- a/Core/nbproject/project.properties +++ b/Core/nbproject/project.properties @@ -6,5 +6,5 @@ license.file=../LICENSE-2.0.txt nbm.homepage=http://www.sleuthkit.org/ nbm.module.author=Brian Carrier nbm.needs.restart=true -spec.version.base=5.1 +spec.version.base=5.2 diff --git a/Core/src/org/sleuthkit/autopsy/corecomponents/AbstractDataResultViewer.java b/Core/src/org/sleuthkit/autopsy/corecomponents/AbstractDataResultViewer.java index 1f7228b212..3387065d1d 100644 --- a/Core/src/org/sleuthkit/autopsy/corecomponents/AbstractDataResultViewer.java +++ b/Core/src/org/sleuthkit/autopsy/corecomponents/AbstractDataResultViewer.java @@ -30,6 +30,7 @@ import org.openide.explorer.ExplorerManager; import org.openide.explorer.ExplorerManager.Provider; import org.openide.nodes.Node; import org.openide.util.Lookup; +import org.sleuthkit.autopsy.casemodule.Case; import org.sleuthkit.autopsy.corecomponentinterfaces.DataContent; import org.sleuthkit.autopsy.corecomponentinterfaces.DataResultViewer; import org.sleuthkit.autopsy.coreutils.Logger; @@ -43,10 +44,9 @@ public abstract class AbstractDataResultViewer extends JPanel implements private static final Logger logger = Logger.getLogger(AbstractDataResultViewer.class.getName()); protected transient ExplorerManager em = new ExplorerManager(); private PropertyChangeListener nodeSelListener; - /** - * Content viewer to respond to selection events - * Either the main one, or custom one if set + * Content viewer to respond to selection events Either the main one, or + * custom one if set */ protected DataContent contentViewer; @@ -54,17 +54,22 @@ public abstract class AbstractDataResultViewer extends JPanel implements //DataContent is designed to return only the default viewer from lookup //use the default one unless set otherwise - contentViewer = Lookup.getDefault().lookup(DataContent.class); - + contentViewer = Lookup.getDefault().lookup(DataContent.class); + //property listener to send nodes to content viewer nodeSelListener = new PropertyChangeListener() { - /** * Propagates changes in the current select node from the * DataResultViewer to the DataContentTopComponent */ @Override public void propertyChange(PropertyChangeEvent evt) { + if (!Case.isCaseOpen()) { + //handle in-between condition when case is being closed + //and legacy selection events are pumped + return; + } + String changed = evt.getPropertyName(); // change that should affect view @@ -77,10 +82,10 @@ public abstract class AbstractDataResultViewer extends JPanel implements setCursor(Cursor.getPredefinedCursor(Cursor.WAIT_CURSOR)); try { Node selectedNode = getSelectedNode(); - + nodeSelected(selectedNode); - + if (selectedNode != null) { // there's a new/changed node to display @@ -142,10 +147,11 @@ public abstract class AbstractDataResultViewer extends JPanel implements @Override public void resetComponent() { } - + /** - * Called when a new node has been selected in the result viewer - * Can update the viewer, etc. + * Called when a new node has been selected in the result viewer Can update + * the viewer, etc. + * * @param selectedNode the new node currently selected */ public abstract void nodeSelected(Node selectedNode); @@ -168,7 +174,7 @@ public abstract class AbstractDataResultViewer extends JPanel implements logger.log(Level.WARNING, "Couldn't set selected nodes.", ex); } } - + @Override public void setContentViewer(DataContent contentViewer) { this.contentViewer = contentViewer; diff --git a/Core/src/org/sleuthkit/autopsy/corecomponents/DataContentViewerMedia.java b/Core/src/org/sleuthkit/autopsy/corecomponents/DataContentViewerMedia.java index d44324d048..2611fe275d 100644 --- a/Core/src/org/sleuthkit/autopsy/corecomponents/DataContentViewerMedia.java +++ b/Core/src/org/sleuthkit/autopsy/corecomponents/DataContentViewerMedia.java @@ -93,6 +93,7 @@ public class DataContentViewerMedia extends javax.swing.JPanel implements DataCo private BufferedImage currentImage = null; private boolean gstInited = false; private AbstractFile lastFile; + private boolean inImageMode; //keeps track if already in image mode to minimize UI setup /** * Creates new form DataContentViewerVideo @@ -103,6 +104,7 @@ public class DataContentViewerMedia extends javax.swing.JPanel implements DataCo } private void customizeComponents() { + inImageMode = false; Platform.setImplicitExit(false); PlatformImpl.startup(new Runnable() { @@ -112,17 +114,17 @@ public class DataContentViewerMedia extends javax.swing.JPanel implements DataCo } }); logger.log(Level.INFO, "Supported image formats by javafx image viewer: "); - + //initialize supported image types //TODO use mime-types instead once we have support String[] fxSupportedImagesSuffixes = ImageIO.getReaderFileSuffixes(); IMAGES = new String[fxSupportedImagesSuffixes.length]; - for (int i=0; i { private static class IsSupportedContentVisitor extends ContentVisitor.Default { - private static final List SUPP_EXTENSIONS = Arrays.asList(".jpeg", ".jpg", ".gif", ".png"); + private final List SUPP_EXTENSIONS; + + IsSupportedContentVisitor() { + String[] supportedImagesSuffixes = ImageIO.getReaderFileSuffixes(); + + SUPP_EXTENSIONS = new ArrayList(supportedImagesSuffixes.length); + for (int i = 0; i < supportedImagesSuffixes.length; ++i) { + String suffix = supportedImagesSuffixes[i]; + SUPP_EXTENSIONS.add("." + suffix); + } + } @Override public Boolean visit(DerivedFile f) { diff --git a/Core/src/org/sleuthkit/autopsy/corecomponents/ThumbnailViewNode.java b/Core/src/org/sleuthkit/autopsy/corecomponents/ThumbnailViewNode.java index 97d23f267f..92ee76109f 100644 --- a/Core/src/org/sleuthkit/autopsy/corecomponents/ThumbnailViewNode.java +++ b/Core/src/org/sleuthkit/autopsy/corecomponents/ThumbnailViewNode.java @@ -26,6 +26,7 @@ import java.awt.Toolkit; import java.awt.image.BufferedImage; import java.io.File; import java.io.IOException; +import java.io.InputStream; import java.lang.ref.SoftReference; import java.util.logging.Level; import javax.imageio.ImageIO; @@ -34,33 +35,41 @@ import javax.swing.JFrame; import org.openide.nodes.Children; import org.openide.nodes.FilterNode; import org.openide.nodes.Node; +import org.openide.util.Exceptions; import org.sleuthkit.autopsy.casemodule.Case; +import org.sleuthkit.autopsy.corelibs.ScalrWrapper; import org.sleuthkit.autopsy.coreutils.Logger; import org.sleuthkit.datamodel.Content; +import org.sleuthkit.datamodel.ReadContentInputStream; import org.sleuthkit.datamodel.TskException; /** - * Node that wraps around original node and adds the bitmap icon representing the picture + * Node that wraps around original node and adds the bitmap icon representing + * the picture */ class ThumbnailViewNode extends FilterNode { private SoftReference iconCache; - private static final Image defaultIcon = new ImageIcon("/org/sleuthkit/autopsy/images/file-icon.png").getImage(); + private static final Logger logger = Logger.getLogger(ThumbnailViewNode.class.getName()); + //private final BufferedImage defaultIconBI; - /** the constructor */ + /** + * the constructor + */ ThumbnailViewNode(Node arg) { super(arg, Children.LEAF); } @Override - public String getDisplayName(){ - if(super.getDisplayName().length() > 15) + public String getDisplayName() { + if (super.getDisplayName().length() > 15) { return super.getDisplayName().substring(0, 15).concat("..."); - else + } else { return super.getDisplayName(); + } } - + @Override public Image getIcon(int type) { Image icon = null; @@ -68,12 +77,11 @@ class ThumbnailViewNode extends FilterNode { if (iconCache != null) { icon = iconCache.get(); } - - - + + if (icon == null) { Content content = this.getLookup().lookup(Content.class); - + if (content != null) { if (getFile(content.getId()).exists()) { try { @@ -84,85 +92,56 @@ class ThumbnailViewNode extends FilterNode { } else { try { icon = generateIcon(content); - ImageIO.write(toBufferedImage(icon), "jpg", getFile(content.getId())); - } catch (TskException ex) { - icon = ThumbnailViewNode.defaultIcon; + if (icon == null) { + icon = ThumbnailViewNode.defaultIcon; + } else { + ImageIO.write((BufferedImage) icon, "jpg", getFile(content.getId())); + } } catch (IOException ex) { + logger.log(Level.WARNING, "Could not write cache thumbnail: " + content, ex); } } } else { icon = ThumbnailViewNode.defaultIcon; } - + iconCache = new SoftReference(icon); } return icon; } - static private Image generateIcon(Content content) throws TskException { - byte[] data = new byte[(int)content.getSize()]; - int bytesRead = content.read(data, 0, content.getSize()); - - if (bytesRead < 1) + /* + * Generate a scaled image + */ + static private BufferedImage generateIcon(Content content) { + + InputStream inputStream = null; + try { + inputStream = new ReadContentInputStream(content); + BufferedImage bi = ImageIO.read(inputStream); + BufferedImage biScaled = ScalrWrapper.resizeFast(bi, 100, 100); + return biScaled; + }catch (OutOfMemoryError e) { + logger.log(Level.WARNING, "Could not scale image (too large): " + content.getName(), e); return null; - - Image result = Toolkit.getDefaultToolkit().createImage(data); + } + catch (Exception e) { + logger.log(Level.WARNING, "Could not scale image: " + content.getName(), e); + return null; + } finally { + if (inputStream != null) { + try { + inputStream.close(); + } catch (IOException ex) { + logger.log(Level.WARNING, "Could not close input stream after resizing thumbnail: " + content.getName(), ex); + } + } - // scale the image - MediaTracker mTracker = new MediaTracker(new JFrame()); - mTracker.addImage(result, 1); - try { - mTracker.waitForID(1); - } catch (InterruptedException ex) { - // TODO: maybe make bubble instead - Logger.getLogger(ThumbnailViewNode.class.getName()).log(Level.WARNING, "Error while trying to scale the icon.", ex); } - int width = result.getWidth(null); - int height = result.getHeight(null); - - int max = Math.max(width, height); - double scale = (75 * 100) / max; - - // getScaledInstance can't take have width or height be 0, so round - // up by adding 1 after truncating to int. - width = (int) ((width * scale) / 100) + 1; - height = (int) ((height * scale) / 100) + 1; - - result = result.getScaledInstance(width, height, Image.SCALE_SMOOTH); - - // load the image completely - mTracker.addImage(result, 1); - try { - mTracker.waitForID(1); - } catch (InterruptedException ex) { - // TODO: maybe make bubble instead - Logger.getLogger(ThumbnailViewNode.class.getName()).log(Level.WARNING, "Error while trying to load the icon.", ex); - } - - // create 75x75 image for the icon with the icon on the center - BufferedImage combined = new BufferedImage(75, 75, BufferedImage.TYPE_INT_ARGB); - Graphics2D g = (Graphics2D) combined.getGraphics(); - g.setColor(Color.WHITE); - g.setBackground(Color.WHITE); - g.drawImage(result, (75 - width) / 2, (75 - height) / 2, null); - - return Toolkit.getDefaultToolkit().createImage(combined.getSource()); } - private static BufferedImage toBufferedImage(Image src) { - int w = src.getWidth(null); - int h = src.getHeight(null); - int type = BufferedImage.TYPE_INT_RGB; // other options - BufferedImage dest = new BufferedImage(w, h, type); - Graphics2D g2 = dest.createGraphics(); - g2.drawImage(src, 0, 0, null); - g2.dispose(); - return dest; - } - private static File getFile(long id) { return new File(Case.getCurrentCase().getCacheDirectory() + File.separator + id + ".jpg"); } - } diff --git a/Core/src/org/sleuthkit/autopsy/directorytree/DirectoryTreeTopComponent.java b/Core/src/org/sleuthkit/autopsy/directorytree/DirectoryTreeTopComponent.java index ed779dcca4..ec74c5a212 100644 --- a/Core/src/org/sleuthkit/autopsy/directorytree/DirectoryTreeTopComponent.java +++ b/Core/src/org/sleuthkit/autopsy/directorytree/DirectoryTreeTopComponent.java @@ -595,14 +595,18 @@ public final class DirectoryTreeTopComponent extends TopComponent implements Dat /** * Event handler to run when selection changed - * + * * TODO this needs to be revised - * + * * @param oldNodes - * @param newNodes + * @param newNodes */ private void respondSelection(final Node[] oldNodes, final Node[] newNodes) { - + if (!Case.isCaseOpen()) { + //handle in-between condition when case is being closed + //and legacy selection events are pumped + return; + } //this looks redundant? // if (getSelectedNode() == null && oldNodes != null) { // try { diff --git a/Core/src/org/sleuthkit/autopsy/ingest/IngestManager.java b/Core/src/org/sleuthkit/autopsy/ingest/IngestManager.java index a903fa14fa..848aef6b82 100644 --- a/Core/src/org/sleuthkit/autopsy/ingest/IngestManager.java +++ b/Core/src/org/sleuthkit/autopsy/ingest/IngestManager.java @@ -899,7 +899,14 @@ public class IngestManager { logger.log(Level.SEVERE, "Error: unexpected exception from module: " + module.getName(), e); stats.addError(module); } - } + catch (OutOfMemoryError e) { + logger.log(Level.SEVERE, "Error: out of memory from module: " + module.getName(), e); + stats.addError(module); + } + } //end for every module + + //free the internal file resource after done with every module + fileToProcess.close(); int newTotalEnqueuedFiles = fileScheduler.getFilesEnqueuedEst(); if (newTotalEnqueuedFiles > totalEnqueuedFiles) { @@ -915,7 +922,8 @@ public class IngestManager { } //--totalEnqueuedFiles; - } //end of this AbstractFile + + } //end of for every AbstractFile logger.log(Level.INFO, "IngestManager: Finished processing files"); return null; } @@ -933,8 +941,7 @@ public class IngestManager { } logger.log(Level.INFO, PlatformUtil.getAllMemUsageInfo()); - logger.log(Level.INFO, "Freeing resources post file pipeline run"); - System.gc(); + logger.log(Level.INFO, "Freeing jvm heap resources post file pipeline run"); System.gc(); logger.log(Level.INFO, PlatformUtil.getAllMemUsageInfo()); diff --git a/CoreLibs/src/org/sleuthkit/autopsy/corelibs/ScalrWrapper.java b/CoreLibs/src/org/sleuthkit/autopsy/corelibs/ScalrWrapper.java index 7b74ee1f10..fce800e30b 100644 --- a/CoreLibs/src/org/sleuthkit/autopsy/corelibs/ScalrWrapper.java +++ b/CoreLibs/src/org/sleuthkit/autopsy/corelibs/ScalrWrapper.java @@ -30,11 +30,11 @@ import org.imgscalr.Scalr.Method; public class ScalrWrapper { public static synchronized BufferedImage resize(BufferedImage input, int width, int height) { - return Scalr.resize(input, width, height); + return Scalr.resize(input, width, height, Scalr.OP_ANTIALIAS); } public static synchronized BufferedImage resize(BufferedImage input, int size) { - return Scalr.resize(input, size); + return Scalr.resize(input, size, Scalr.OP_ANTIALIAS); } public static synchronized BufferedImage resizeHighQuality(BufferedImage input, int width, int height) { @@ -42,6 +42,10 @@ public class ScalrWrapper { } public static synchronized BufferedImage resizeFast(BufferedImage input, int size) { - return Scalr.resize(input, Method.SPEED, size, Scalr.OP_ANTIALIAS); + return Scalr.resize(input, Method.SPEED, Scalr.Mode.AUTOMATIC, size, Scalr.OP_ANTIALIAS); + } + + public static synchronized BufferedImage resizeFast(BufferedImage input, int width, int height) { + return Scalr.resize(input, Method.SPEED, Scalr.Mode.AUTOMATIC, width, height, Scalr.OP_ANTIALIAS); } } diff --git a/ExifParser/manifest.mf b/ExifParser/manifest.mf index 63167f23a4..bed881d309 100644 --- a/ExifParser/manifest.mf +++ b/ExifParser/manifest.mf @@ -1,6 +1,6 @@ Manifest-Version: 1.0 AutoUpdate-Show-In-Client: true OpenIDE-Module: org.sleuthkit.autopsy.exifparser/3 -OpenIDE-Module-Implementation-Version: 6 +OpenIDE-Module-Implementation-Version: 7 OpenIDE-Module-Layer: org/sleuthkit/autopsy/exifparser/layer.xml OpenIDE-Module-Localizing-Bundle: org/sleuthkit/autopsy/exifparser/Bundle.properties diff --git a/ExifParser/nbproject/project.properties b/ExifParser/nbproject/project.properties index 6f48597d02..f278b6de1b 100644 --- a/ExifParser/nbproject/project.properties +++ b/ExifParser/nbproject/project.properties @@ -3,4 +3,4 @@ javac.compilerargs=-Xlint -Xlint:-serial license.file=../LICENSE-2.0.txt nbm.homepage=http://www.sleuthkit.org/autopsy/ nbm.needs.restart=true -spec.version.base=1.0 +spec.version.base=1.1 diff --git a/ExifParser/nbproject/project.xml b/ExifParser/nbproject/project.xml index 6f5571f491..8bc8dc2781 100644 --- a/ExifParser/nbproject/project.xml +++ b/ExifParser/nbproject/project.xml @@ -12,7 +12,7 @@ 7 - 5.0 + 5.2 diff --git a/HashDatabase/manifest.mf b/HashDatabase/manifest.mf index afdd2b92bd..3b7e745580 100644 --- a/HashDatabase/manifest.mf +++ b/HashDatabase/manifest.mf @@ -1,7 +1,7 @@ Manifest-Version: 1.0 AutoUpdate-Show-In-Client: true OpenIDE-Module: org.sleuthkit.autopsy.hashdatabase/3 -OpenIDE-Module-Implementation-Version: 6 +OpenIDE-Module-Implementation-Version: 7 OpenIDE-Module-Layer: org/sleuthkit/autopsy/hashdatabase/layer.xml OpenIDE-Module-Localizing-Bundle: org/sleuthkit/autopsy/hashdatabase/Bundle.properties diff --git a/HashDatabase/nbproject/project.properties b/HashDatabase/nbproject/project.properties index f278b6de1b..0735c621fa 100644 --- a/HashDatabase/nbproject/project.properties +++ b/HashDatabase/nbproject/project.properties @@ -3,4 +3,4 @@ javac.compilerargs=-Xlint -Xlint:-serial license.file=../LICENSE-2.0.txt nbm.homepage=http://www.sleuthkit.org/autopsy/ nbm.needs.restart=true -spec.version.base=1.1 +spec.version.base=1.2 diff --git a/HashDatabase/nbproject/project.xml b/HashDatabase/nbproject/project.xml index fb4111eff7..373b6a39f7 100644 --- a/HashDatabase/nbproject/project.xml +++ b/HashDatabase/nbproject/project.xml @@ -78,7 +78,7 @@ 7 - 5.0 + 5.2 diff --git a/KeywordSearch/manifest.mf b/KeywordSearch/manifest.mf index 4965047fae..693f2caa22 100644 --- a/KeywordSearch/manifest.mf +++ b/KeywordSearch/manifest.mf @@ -1,7 +1,7 @@ Manifest-Version: 1.0 AutoUpdate-Show-In-Client: true OpenIDE-Module: org.sleuthkit.autopsy.keywordsearch/5 -OpenIDE-Module-Implementation-Version: 6 +OpenIDE-Module-Implementation-Version: 7 OpenIDE-Module-Install: org/sleuthkit/autopsy/keywordsearch/Installer.class OpenIDE-Module-Layer: org/sleuthkit/autopsy/keywordsearch/layer.xml OpenIDE-Module-Localizing-Bundle: org/sleuthkit/autopsy/keywordsearch/Bundle.properties diff --git a/KeywordSearch/nbproject/project.properties b/KeywordSearch/nbproject/project.properties index 48e8b51efd..080c464e8a 100644 --- a/KeywordSearch/nbproject/project.properties +++ b/KeywordSearch/nbproject/project.properties @@ -3,4 +3,4 @@ javac.compilerargs=-Xlint -Xlint:-serial license.file=../LICENSE-2.0.txt nbm.homepage=http://www.sleuthkit.org/autopsy/ nbm.needs.restart=true -spec.version.base=3.0 +spec.version.base=3.1 diff --git a/KeywordSearch/nbproject/project.xml b/KeywordSearch/nbproject/project.xml index 28c7220f9b..7684af33da 100644 --- a/KeywordSearch/nbproject/project.xml +++ b/KeywordSearch/nbproject/project.xml @@ -96,7 +96,7 @@ 7 - 5.0 + 5.2 diff --git a/KeywordSearch/src/org/sleuthkit/autopsy/keywordsearch/AbstractFileExtract.java b/KeywordSearch/src/org/sleuthkit/autopsy/keywordsearch/AbstractFileExtract.java index c196533875..dcb8b25728 100644 --- a/KeywordSearch/src/org/sleuthkit/autopsy/keywordsearch/AbstractFileExtract.java +++ b/KeywordSearch/src/org/sleuthkit/autopsy/keywordsearch/AbstractFileExtract.java @@ -16,9 +16,9 @@ * See the License for the specific language governing permissions and * limitations under the License. */ - package org.sleuthkit.autopsy.keywordsearch; +import java.util.Arrays; import java.util.List; import java.util.Map; import org.sleuthkit.autopsy.coreutils.StringExtract.StringExtractUnicodeTable.SCRIPT; @@ -29,72 +29,124 @@ import org.sleuthkit.datamodel.AbstractFile; * chunks */ interface AbstractFileExtract { - + /** * Common options that can be used by some extractors */ enum ExtractOptions { + EXTRACT_UTF16, ///< extract UTF16 text, possible values Boolean.TRUE.toString(), Boolean.FALSE.toString() EXTRACT_UTF8, ///< extract UTF8 text, possible values Boolean.TRUE.toString(), Boolean.FALSE.toString() }; + + //generally text extractors should ignore archives + //and let unpacking modules take case of them + static final List ARCHIVE_MIME_TYPES = + Arrays.asList( + //ignore unstructured binary and compressed data, for which string extraction or unzipper works better + "application/x-7z-compressed", + "application/x-ace-compressed", + "application/x-alz-compressed", + "application/x-arj", + "application/vnd.ms-cab-compressed", + "application/x-cfs-compressed", + "application/x-dgc-compressed", + "application/x-apple-diskimage", + "application/x-gca-compressed", + "application/x-dar", + "application/x-lzx", + "application/x-lzh", + "application/x-rar-compressed", + "application/x-stuffit", + "application/x-stuffitx", + "application/x-gtar", + "application/x-archive", + "application/x-executable", + "application/x-gzip", + "application/zip", + "application/x-zoo", + "application/x-cpio", + "application/x-shar", + "application/x-tar", + "application/x-bzip", + "application/x-bzip2", + "application/x-lzip", + "application/x-lzma", + "application/x-lzop", + "application/x-z", + "application/x-compress"); /** * Get number of chunks resulted from extracting this AbstractFile + * * @return the number of chunks produced */ int getNumChunks(); /** * Get the source file associated with this extraction + * * @return the source AbstractFile */ AbstractFile getSourceFile(); /** * Index the Abstract File + * * @param sourceFile file to index * @return true if indexed successfully, false otherwise - * @throws org.sleuthkit.autopsy.keywordsearch.Ingester.IngesterException + * @throws org.sleuthkit.autopsy.keywordsearch.Ingester.IngesterException */ boolean index(AbstractFile sourceFile) throws Ingester.IngesterException; - + /** * Sets the scripts to use for the extraction + * * @param extractScripts scripts to use - * @return true if extractor supports script - specific extraction, false otherwise + * @return true if extractor supports script - specific extraction, false + * otherwise */ boolean setScripts(List