content: /*
   * Copyright 2002-2019 Drew Noakes and contributors
   *
   *    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
   *
   *        https://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.
   *
   * More information about this project is available at:
   *
   *    https://drewnoakes.com/code/exif/
   *    https://github.com/drewnoakes/metadata-extractor
   */
  
  package com.drew.tools;
  
  import com.adobe.internal.xmp.XMPException;
  import com.adobe.internal.xmp.XMPIterator;
  import com.adobe.internal.xmp.XMPMeta;
  import com.adobe.internal.xmp.options.IteratorOptions;
  import com.adobe.internal.xmp.properties.XMPPropertyInfo;
  import com.drew.imaging.FileType;
  import com.drew.imaging.FileTypeDetector;
  import com.drew.imaging.ImageMetadataReader;
  import com.drew.lang.StringUtil;
  import com.drew.lang.annotations.NotNull;
  import com.drew.lang.annotations.Nullable;
  import com.drew.metadata.Directory;
  import com.drew.metadata.Metadata;
  import com.drew.metadata.Tag;
  import com.drew.metadata.exif.ExifIFD0Directory;
  import com.drew.metadata.exif.ExifSubIFDDirectory;
  import com.drew.metadata.exif.ExifThumbnailDirectory;
  import com.drew.metadata.file.FileSystemDirectory;
  import com.drew.metadata.xmp.XmpDirectory;
  
  import java.io.*;
  import java.util.*;
  
  /**
   * @author Drew Noakes https://drewnoakes.com
   */
  public class ProcessAllImagesInFolderUtility
  {
      public static void main(String[] args) throws IOException
      {
          Locale.setDefault(new Locale("en", "US"));
          System.setProperty("user.timezone", "Australia/Sydney");
  
          List<String> directories = new ArrayList<String>();
  
          FileHandler handler = null;
          PrintStream log = System.out;
  
          for (int i = 0; i < args.length; i++) {
              String arg = args[i];
              if (arg.equalsIgnoreCase("--text")) {
                  // If "--text" is specified, write the discovered metadata into a sub-folder relative to the image
                  handler = new TextFileOutputHandler();
              } else if (arg.equalsIgnoreCase("--markdown")) {
                  // If "--markdown" is specified, write a summary table in markdown format to standard out
                  handler = new MarkdownTableOutputHandler();
              } else if (arg.equalsIgnoreCase("--unknown")) {
                  // If "--unknown" is specified, write CSV tallying unknown tag counts
                  handler = new UnknownTagHandler();
              } else if (arg.equalsIgnoreCase("--log-file")) {
                  if (i == args.length - 1) {
                      printUsage();
                      System.exit(1);
                  }
                  log = new PrintStream(new FileOutputStream(args[++i], false), true);
              } else {
                  // Treat this argument as a directory
                  directories.add(arg);
              }
          }
  
          if (directories.isEmpty()) {
              System.err.println("Expects one or more directories as arguments.");
              printUsage();
              System.exit(1);
          }
  
          if (handler == null) {
              handler = new BasicFileHandler();
          }
  
          long start = System.nanoTime();
  
          for (String directory : directories) {
              processDirectory(new File(directory), handler, "", log);
          }
  
          handler.onScanCompleted(log);
  
          System.out.println(String.format("Completed in %d ms", (System.nanoTime() - start) / 1000000));
  
          if (log != System.out) {
              log.close();
          }
      }
  
      private static void printUsage()
      {
          System.out.println("Usage:");
          System.out.println();
          System.out.println("  java com.drew.tools.ProcessAllImagesInFolderUtility [--text|--markdown|--unknown] [--log-file <file-name>]");
      }
  
      private static void processDirectory(@NotNull File path, @NotNull FileHandler handler, @NotNull String relativePath, PrintStream log)
      {
          handler.onStartingDirectory(path);
  
          String[] pathItems = path.list();
  
          if (pathItems == null) {
              return;
          }
  
          // Order alphabetically so that output is stable across invocations
          Arrays.sort(pathItems);
  
          for (String pathItem : pathItems) {
              File file = new File(path, pathItem);
  
              if (file.isDirectory()) {
                  processDirectory(file, handler, relativePath.length() == 0 ? pathItem : relativePath + "/" + pathItem, log);
              } else if (handler.shouldProcess(file)) {
  
                  handler.onBeforeExtraction(file, log, relativePath);
  
                  // Read metadata
                  final Metadata metadata;
                  try {
                      metadata = ImageMetadataReader.readMetadata(file);
                  } catch (Throwable t) {
                      handler.onExtractionError(file, t, log);
                      continue;
                  }
  
                  handler.onExtractionSuccess(file, metadata, relativePath, log);
              }
          }
      }
  
      interface FileHandler
      {
          /** Called when the scan is about to start processing files in directory <code>path</code>. */
          void onStartingDirectory(@NotNull File directoryPath);
  
          /** Called to determine whether the implementation should process <code>filePath</code>. */
          boolean shouldProcess(@NotNull File file);
  
          /** Called before extraction is performed on <code>filePath</code>. */
          void onBeforeExtraction(@NotNull File file, @NotNull PrintStream log, @NotNull String relativePath);
  
          /** Called when extraction on <code>filePath</code> completed without an exception. */
          void onExtractionSuccess(@NotNull File file, @NotNull Metadata metadata, @NotNull String relativePath, @NotNull PrintStream log);
  
          /** Called when extraction on <code>filePath</code> resulted in an exception. */
          void onExtractionError(@NotNull File file, @NotNull Throwable throwable, @NotNull PrintStream log);
  
          /** Called when all files have been processed. */
          void onScanCompleted(@NotNull PrintStream log);
      }
  
      abstract static class FileHandlerBase implements FileHandler
      {
          // TODO obtain these from FileType enum directly
          private final Set<String> _supportedExtensions = new HashSet<String>(
              Arrays.asList(
                  "3fr",
                  "3g2",
                  "3gp",
                  "ai",
                  "arw",
                  "avi",
                  "avif",
                  "bmp",
                  "cam",
                  "cr2",
                  "cr3",
                  "crw",
                  "dcr",
                  "dng",
                  "eps",
                  "fuzzed",
                  "gif",
                  "gpr",
                  "heic",
                  "heif",
                  "ico",
                  "j2c",
                  "jp2",
                  "jpeg",
                  "jpf",
                  "jpg",
                  "jpm",
                  "jxr",
                  "kdc",
                  "m2ts",
                  "m2v",
                  "m4a",
                  "m4v",
                  "mj2",
                  "mov",
                  "mp3",
                  "mp4",
                  "mpg",
                  "mts",
                  "nef",
                  "orf",
                  "pbm",
                  "pcx",
                  "pef",
                  "pgm",
                  "png",
                  "pnm",
                  "ppm",
                  "psd",
                  "raf",
                  "rw2",
                  "rwl",
                  "srw",
                  "tif",
                  "tiff",
                  "wav",
                  "webp",
                  "x3f"));
  
          private int _processedFileCount = 0;
          private int _exceptionCount = 0;
          private int _errorCount = 0;
          private long _processedByteCount = 0;
  
          public void onStartingDirectory(@NotNull File directoryPath)
          {}
  
          public boolean shouldProcess(@NotNull File file)
          {
              String extension = getExtension(file);
              return extension != null && _supportedExtensions.contains(extension.toLowerCase());
          }
  
          public void onBeforeExtraction(@NotNull File file, @NotNull PrintStream log, @NotNull String relativePath)
          {
              _processedFileCount++;
              _processedByteCount += file.length();
          }
  
          public void onExtractionError(@NotNull File file, @NotNull Throwable throwable, @NotNull PrintStream log)
          {
              _exceptionCount++;
              log.printf("\t[%s] %s\n", throwable.getClass().getName(), throwable.getMessage());
          }
  
          public void onExtractionSuccess(@NotNull File file, @NotNull Metadata metadata, @NotNull String relativePath, @NotNull PrintStream log)
          {
              if (metadata.hasErrors()) {
                  log.print(file);
                  log.print('\n');
                  for (Directory directory : metadata.getDirectories()) {
                      if (!directory.hasErrors())
                          continue;
                      for (String error : directory.getErrors()) {
                          log.printf("\t[%s] %s\n", directory.getName(), error);
                          _errorCount++;
                      }
                  }
              }
          }
  
          public void onScanCompleted(@NotNull PrintStream log)
          {
              if (_processedFileCount > 0) {
                  log.print(String.format(
                      "Processed %,d files (%,d bytes) with %,d exceptions and %,d file errors\n",
                      _processedFileCount, _processedByteCount, _exceptionCount, _errorCount
                  ));
              }
          }
  
          @Nullable
          protected String getExtension(@NotNull File file)
          {
              String fileName = file.getName();
              int i = fileName.lastIndexOf('.');
              if (i == -1)
                  return null;
              if (i == fileName.length() - 1)
                  return null;
              return fileName.substring(i + 1);
          }
      }
  
      /**
       * Writes a text file containing the extracted metadata for each input file.
       */
      static class TextFileOutputHandler extends FileHandlerBase
      {
          /** Standardise line ending so that generated files can be more easily diffed. */
          private static final String NEW_LINE = "\n";
  
          @Override
          public void onStartingDirectory(@NotNull File directoryPath)
          {
              super.onStartingDirectory(directoryPath);
  
              // Delete any existing 'metadata' folder
              File metadataDirectory = new File(directoryPath + "/metadata/java");
              if (metadataDirectory.exists())
                  deleteRecursively(metadataDirectory);
          }
  
          private static void deleteRecursively(@NotNull File directory)
          {
              if (!directory.isDirectory())
                  throw new IllegalArgumentException("Must be a directory.");
  
              if (directory.exists()) {
                  String[] list = directory.list();
                  if (list != null) {
                      for (String item : list) {
                          File file = new File(item);
                          if (file.isDirectory())
                              deleteRecursively(file);
                          else
                              file.delete();
                      }
                  }
              }
  
              directory.delete();
          }
  
          @Override
          public void onBeforeExtraction(@NotNull File file, @NotNull PrintStream log, @NotNull String relativePath)
          {
              super.onBeforeExtraction(file, log, relativePath);
              log.print(file.getAbsoluteFile());
              log.print(NEW_LINE);
          }
  
          @Override
          public void onExtractionSuccess(@NotNull File file, @NotNull Metadata metadata, @NotNull String relativePath, @NotNull PrintStream log)
          {
              super.onExtractionSuccess(file, metadata, relativePath, log);
  
              try {
                  PrintWriter writer = null;
                  try
                  {
                      writer = openWriter(file);
  
                      // Write any errors
                      if (metadata.hasErrors()) {
                          for (Directory directory : metadata.getDirectories()) {
                              if (!directory.hasErrors())
                                  continue;
                              for (String error : directory.getErrors())
                                  writer.format("[ERROR: %s] %s%s", directory.getName(), error, NEW_LINE);
                          }
                          writer.write(NEW_LINE);
                      }
  
                      // Write tag values for each directory
                      for (Directory directory : metadata.getDirectories()) {
                          String directoryName = directory.getName();
                          // Write the directory's tags
                          for (Tag tag : directory.getTags()) {
                              String tagName = tag.getTagName();
                              String description;
                              try {
                                  description = tag.getDescription();
                              } catch (Exception ex) {
                                  description = "ERROR: " + ex.getMessage();
                              }
                              if (description == null)
                                  description = "";
                              // Skip the file write-time as this changes based on the time at which the regression test image repository was cloned
                              if (directory instanceof FileSystemDirectory && tag.getTagType() == FileSystemDirectory.TAG_FILE_MODIFIED_DATE)
                                  description = "<omitted for regression testing as checkout dependent>";
                              writer.format("[%s - %s] %s = %s%s", directoryName, tag.getTagTypeHex(), tagName, description, NEW_LINE);
                          }
                          if (directory.getTagCount() != 0)
                              writer.write(NEW_LINE);
                          // Special handling for XMP directory data
                          if (directory instanceof XmpDirectory) {
                              boolean wrote = false;
                              XmpDirectory xmpDirectory = (XmpDirectory)directory;
                              XMPMeta xmpMeta = xmpDirectory.getXMPMeta();
                              try {
                                  IteratorOptions options = new IteratorOptions().setJustLeafnodes(true);
                                  XMPIterator iterator = xmpMeta.iterator(options);
                                  while (iterator.hasNext()) {
                                      XMPPropertyInfo prop = (XMPPropertyInfo)iterator.next();
                                      String ns = prop.getNamespace();
                                      String path = prop.getPath();
                                      String value = prop.getValue();
  
                                      if (path == null)
                                          continue;
                                      if (ns == null)
                                          ns = "";
  
                                      final int MAX_XMP_VALUE_LENGTH = 512;
                                      if (value == null)
                                          value = "";
                                      else if (value.length() > MAX_XMP_VALUE_LENGTH)
                                          value = String.format("%s <truncated from %d characters>", value.substring(0, MAX_XMP_VALUE_LENGTH), value.length());
  
                                      writer.format("[XMPMeta - %s] %s = %s%s", ns, path, value, NEW_LINE);
                                      wrote = true;
                                  }
                              } catch (XMPException e) {
                                  e.printStackTrace();
                              }
                              if (wrote)
                                  writer.write(NEW_LINE);
                          }
                      }
  
                      // Write file structure
                      writeHierarchyLevel(metadata, writer, null, 0);
  
                      writer.write(NEW_LINE);
                  } finally {
                      closeWriter(writer);
                  }
              } catch (IOException e) {
                  e.printStackTrace();
              }
          }
  
          private static void writeHierarchyLevel(@NotNull Metadata metadata, @NotNull PrintWriter writer, @Nullable Directory parent, int level)
          {
              final int indent = 4;
  
              for (Directory child : metadata.getDirectories()) {
                  if (parent == null) {
                      if (child.getParent() != null)
                          continue;
                  } else if (!parent.equals(child.getParent())) {
                      continue;
                  }
  
                  for (int i = 0; i < level*indent; i++) {
                      writer.write(' ');
                  }
                  writer.write("- ");
                  writer.write(child.getName());
                  writer.write(NEW_LINE);
                  writeHierarchyLevel(metadata, writer, child, level + 1);
              }
          }
  
          @Override
          public void onExtractionError(@NotNull File file, @NotNull Throwable throwable, @NotNull PrintStream log)
          {
              super.onExtractionError(file, throwable, log);
  
              try {
                  PrintWriter writer = null;
                  try {
                      writer = openWriter(file);
                      writer.write("EXCEPTION: " + throwable.getMessage() + NEW_LINE);
                      writer.write(NEW_LINE);
                  } finally {
                      closeWriter(writer);
                  }
              } catch (IOException e) {
                  log.printf("IO exception writing metadata file: %s%s", e.getMessage(), NEW_LINE);
              }
          }
  
          @NotNull
          private static PrintWriter openWriter(@NotNull File file) throws IOException
          {
              // Create the output directory if it doesn't exist
              File metadataDir = new File(String.format("%s/metadata", file.getParent()));
              if (!metadataDir.exists())
                  metadataDir.mkdir();
  
              File javaDir = new File(String.format("%s/metadata/java", file.getParent()));
              if (!javaDir.exists())
                  javaDir.mkdir();
  
              String outputPath = String.format("%s/metadata/java/%s.txt", file.getParent(), file.getName());
              Writer writer = new OutputStreamWriter(
                  new FileOutputStream(outputPath),
                  "UTF-8"
              );
              writer.write("FILE: " + file.getName() + NEW_LINE);
  
              // Detect file type
              BufferedInputStream stream = null;
              try {
                  stream = new BufferedInputStream(new FileInputStream(file));
                  FileType fileType = FileTypeDetector.detectFileType(stream, file.getName());
                  writer.write(String.format("TYPE: %s" + NEW_LINE, fileType.toString().toUpperCase()));
                  writer.write(NEW_LINE);
              } finally {
                  if (stream != null) {
                      stream.close();
                  }
              }
  
              return new PrintWriter(writer);
          }
  
          private static void closeWriter(@Nullable Writer writer) throws IOException
          {
              if (writer != null) {
                  writer.write("Generated using metadata-extractor" + NEW_LINE);
                  writer.write("https://drewnoakes.com/code/exif/" + NEW_LINE);
                  writer.flush();
                  writer.close();
              }
          }
      }
  
      /**
       * Creates a table describing sample images using Wiki markdown.
       */
      static class MarkdownTableOutputHandler extends FileHandlerBase
      {
          private final Map<String, String> _extensionEquivalence = new HashMap<String, String>();
          private final Map<String, List<Row>> _rowListByExtension = new HashMap<String, List<Row>>();
  
          static class Row
          {
              final File file;
              final Metadata metadata;
              @NotNull final String relativePath;
              @Nullable private String manufacturer;
              @Nullable private String model;
              @Nullable private String exifVersion;
              @Nullable private String thumbnail;
              @Nullable private String makernote;
  
              Row(@NotNull File file, @NotNull Metadata metadata, @NotNull String relativePath)
              {
                  this.file = file;
                  this.metadata = metadata;
                  this.relativePath = relativePath;
  
                  ExifIFD0Directory ifd0Dir = metadata.getFirstDirectoryOfType(ExifIFD0Directory.class);
                  ExifSubIFDDirectory subIfdDir = metadata.getFirstDirectoryOfType(ExifSubIFDDirectory.class);
                  ExifThumbnailDirectory thumbDir = metadata.getFirstDirectoryOfType(ExifThumbnailDirectory.class);
                  if (ifd0Dir != null) {
                      manufacturer = ifd0Dir.getDescription(ExifIFD0Directory.TAG_MAKE);
                      model = ifd0Dir.getDescription(ExifIFD0Directory.TAG_MODEL);
                  }
                  boolean hasMakernoteData = false;
                  if (subIfdDir != null) {
                      exifVersion = subIfdDir.getDescription(ExifSubIFDDirectory.TAG_EXIF_VERSION);
                      hasMakernoteData = subIfdDir.containsTag(ExifSubIFDDirectory.TAG_MAKERNOTE);
                  }
                  if (thumbDir != null) {
                      Integer width = thumbDir.getInteger(ExifThumbnailDirectory.TAG_IMAGE_WIDTH);
                      Integer height = thumbDir.getInteger(ExifThumbnailDirectory.TAG_IMAGE_HEIGHT);
                      thumbnail = width != null && height != null
                          ? String.format("Yes (%s x %s)", width, height)
                          : "Yes";
                  }
                  for (Directory directory : metadata.getDirectories()) {
                      if (directory.getClass().getName().contains("Makernote")) {
                          makernote = directory.getName().replace("Makernote", "").trim();
                          break;
                      }
                  }
                  if (makernote == null) {
                      makernote = hasMakernoteData ? "(Unknown)" : "N/A";
                  }
              }
          }
  
          public MarkdownTableOutputHandler()
          {
              _extensionEquivalence.put("jpeg", "jpg");
          }
  
          @Override
          public void onExtractionSuccess(@NotNull File file, @NotNull Metadata metadata, @NotNull String relativePath, @NotNull PrintStream log)
          {
              super.onExtractionSuccess(file, metadata, relativePath, log);
  
              String extension = getExtension(file);
  
              if (extension == null) {
                  return;
              }
  
              // Sanitise the extension
              extension = extension.toLowerCase();
              if (_extensionEquivalence.containsKey(extension))
                  extension = _extensionEquivalence.get(extension);
  
              List<Row> list = _rowListByExtension.get(extension);
              if (list == null) {
                  list = new ArrayList<Row>();
                  _rowListByExtension.put(extension, list);
              }
              list.add(new Row(file, metadata, relativePath));
          }
  
          @Override
          public void onScanCompleted(@NotNull PrintStream log)
          {
              super.onScanCompleted(log);
  
              OutputStream outputStream = null;
              PrintStream stream = null;
              try {
                  outputStream = new FileOutputStream("../wiki/ImageDatabaseSummary.md", false);
                  stream = new PrintStream(outputStream, false);
                  writeOutput(stream);
                  stream.flush();
              } catch (IOException e) {
                  e.printStackTrace();
              } finally {
                  if (stream != null)
                      stream.close();
                  if (outputStream != null)
                      try {
                          outputStream.close();
                      } catch (IOException e) {
                          e.printStackTrace();
                      }
              }
          }
  
          private void writeOutput(@NotNull PrintStream stream) throws IOException
          {
              Writer writer = new OutputStreamWriter(stream);
              writer.write("# Image Database Summary\n\n");
  
              for (Map.Entry<String, List<Row>> entry : _rowListByExtension.entrySet()) {
                  String extension = entry.getKey();
                  writer.write("## " + extension.toUpperCase() + " Files\n\n");
  
                  writer.write("File|Manufacturer|Model|Dir Count|Exif?|Makernote|Thumbnail|All Data\n");
                  writer.write("----|------------|-----|---------|-----|---------|---------|--------\n");
  
                  List<Row> rows = entry.getValue();
  
                  // Order by manufacturer, then model
                  Collections.sort(rows, new Comparator<Row>() {
                      public int compare(Row o1, Row o2)
                      {
                          int c1 = StringUtil.compare(o1.manufacturer, o2.manufacturer);
                          return c1 != 0 ? c1 : StringUtil.compare(o1.model, o2.model);
                      }
                  });
  
                  for (Row row : rows) {
                      writer.write(String.format("[%s](https://raw.githubusercontent.com/drewnoakes/metadata-extractor-images/master/%s/%s)|%s|%s|%d|%s|%s|%s|[metadata](https://raw.githubusercontent.com/drewnoakes/metadata-extractor-images/master/%s/metadata/%s.txt)\n",
                              row.file.getName(),
                              row.relativePath,
                              StringUtil.urlEncode(row.file.getName()),
                              row.manufacturer == null ? "" : row.manufacturer,
                              row.model == null ? "" : row.model,
                              row.metadata.getDirectoryCount(),
                              row.exifVersion == null ? "" : row.exifVersion,
                              row.makernote == null ? "" : row.makernote,
                              row.thumbnail == null ? "" : row.thumbnail,
                              row.relativePath,
                              StringUtil.urlEncode(row.file.getName()).toLowerCase()
                      ));
                  }
  
                  writer.write('\n');
              }
              writer.flush();
          }
      }
  
      /**
       * Keeps track of unknown tags.
       */
      static class UnknownTagHandler extends FileHandlerBase
      {
          private HashMap<String, HashMap<Integer, Integer>> _occurrenceCountByTagByDirectory = new HashMap<String, HashMap<Integer, Integer>>();
  
          @Override
          public void onExtractionSuccess(@NotNull File file, @NotNull Metadata metadata, @NotNull String relativePath, @NotNull PrintStream log)
          {
              super.onExtractionSuccess(file, metadata, relativePath, log);
  
              for (Directory directory : metadata.getDirectories()) {
                  for (Tag tag : directory.getTags()) {
  
                      // Only interested in unknown tags (those without names)
                      if (tag.hasTagName())
                          continue;
  
                      HashMap<Integer, Integer> occurrenceCountByTag = _occurrenceCountByTagByDirectory.get(directory.getName());
                      if (occurrenceCountByTag == null) {
                          occurrenceCountByTag = new HashMap<Integer, Integer>();
                          _occurrenceCountByTagByDirectory.put(directory.getName(), occurrenceCountByTag);
                      }
  
                      Integer count = occurrenceCountByTag.get(tag.getTagType());
                      if (count == null) {
                          count = 0;
                          occurrenceCountByTag.put(tag.getTagType(), 0);
                      }
  
                      occurrenceCountByTag.put(tag.getTagType(), count + 1);
                  }
              }
          }
  
          @Override
          public void onScanCompleted(@NotNull PrintStream log)
          {
              super.onScanCompleted(log);
  
              for (Map.Entry<String, HashMap<Integer, Integer>> pair1 : _occurrenceCountByTagByDirectory.entrySet()) {
                  String directoryName = pair1.getKey();
                  List<Map.Entry<Integer, Integer>> counts = new ArrayList<Map.Entry<Integer, Integer>>(pair1.getValue().entrySet());
                  Collections.sort(counts, new Comparator<Map.Entry<Integer, Integer>>()
                  {
                      public int compare(Map.Entry<Integer, Integer> o1, Map.Entry<Integer, Integer> o2)
                      {
                          return o2.getValue().compareTo(o1.getValue());
                      }
                  });
                  for (Map.Entry<Integer, Integer> pair2 : counts) {
                      Integer tagType = pair2.getKey();
                      Integer count = pair2.getValue();
                      log.format("%s, 0x%04X, %d\n", directoryName, tagType, count);
                  }
              }
          }
      }
  
      /**
       * Does nothing with the output except enumerate it in memory and format descriptions. This is useful in order to
       * flush out any potential exceptions raised during the formatting of extracted value descriptions.
       */
      static class BasicFileHandler extends FileHandlerBase
      {
          @Override
          public void onExtractionSuccess(@NotNull File file, @NotNull Metadata metadata, @NotNull String relativePath, @NotNull PrintStream log)
          {
              super.onExtractionSuccess(file, metadata, relativePath, log);
  
              // Iterate through all values, calling toString to flush out any formatting exceptions
              for (Directory directory : metadata.getDirectories()) {
                  directory.getName();
                  for (Tag tag : directory.getTags()) {
                      tag.getTagName();
                      tag.getDescription();
                  }
              }
          }
      }
  }
  