/*
 * file:       UniversalProjectReader.java
 * author:     Jon Iles
 * copyright:  (c) Packwood Software 2016
 * date:       2016-10-13
 */

/*
 * This library is free software; you can redistribute it and/or modify it
 * under the terms of the GNU Lesser General Public License as published by the
 * Free Software Foundation; either version 2.1 of the License, or (at your
 * option) any later version.
 *
 * This library is distributed in the hope that it will be useful, but
 * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
 * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public
 * License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public License
 * along with this library; if not, write to the Free Software Foundation, Inc.,
 * 59 Temple Place, Suite 330, Boston, MA 02111-1307, USA.
 */

package net.sf.mpxj.reader;

import java.io.BufferedInputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.InputStream;
import java.nio.charset.Charset;
import java.sql.Connection;
import java.sql.DatabaseMetaData;
import java.sql.DriverManager;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.regex.Pattern;

import org.apache.poi.poifs.filesystem.POIFSFileSystem;
import org.apache.poi.util.CloseIgnoringInputStream;

import net.sf.mpxj.MPXJException;
import net.sf.mpxj.ProjectFile;
import net.sf.mpxj.asta.AstaDatabaseFileReader;
import net.sf.mpxj.asta.AstaDatabaseReader;
import net.sf.mpxj.asta.AstaFileReader;
import net.sf.mpxj.common.AutoCloseableHelper;
import net.sf.mpxj.common.CharsetHelper;
import net.sf.mpxj.common.FileHelper;
import net.sf.mpxj.common.InputStreamHelper;
import net.sf.mpxj.common.StreamHelper;
import net.sf.mpxj.conceptdraw.ConceptDrawProjectReader;
import net.sf.mpxj.fasttrack.FastTrackReader;
import net.sf.mpxj.ganttdesigner.GanttDesignerReader;
import net.sf.mpxj.ganttproject.GanttProjectReader;
import net.sf.mpxj.merlin.MerlinReader;
import net.sf.mpxj.mpd.MPDDatabaseReader;
import net.sf.mpxj.mpp.MPPReader;
import net.sf.mpxj.mpx.MPXReader;
import net.sf.mpxj.mspdi.MSPDIReader;
import net.sf.mpxj.phoenix.PhoenixInputStream;
import net.sf.mpxj.phoenix.PhoenixReader;
import net.sf.mpxj.planner.PlannerReader;
import net.sf.mpxj.primavera.PrimaveraDatabaseFileReader;
import net.sf.mpxj.primavera.PrimaveraPMFileReader;
import net.sf.mpxj.primavera.PrimaveraXERFileReader;
import net.sf.mpxj.primavera.p3.P3DatabaseReader;
import net.sf.mpxj.primavera.p3.P3PRXFileReader;
import net.sf.mpxj.primavera.suretrak.SureTrakDatabaseReader;
import net.sf.mpxj.primavera.suretrak.SureTrakSTXFileReader;
import net.sf.mpxj.projectcommander.ProjectCommanderReader;
import net.sf.mpxj.projectlibre.ProjectLibreReader;
import net.sf.mpxj.sage.SageReader;
import net.sf.mpxj.sdef.SDEFReader;
import net.sf.mpxj.synchro.SynchroReader;
import net.sf.mpxj.turboproject.TurboProjectReader;

/**
 * This class implements a universal project reader: given a file or a stream this reader
 * will sample the content and determine the type of file it has been given. It will then
 * instantiate the correct reader for that file type and proceed to read the file.
 */
public final class UniversalProjectReader extends AbstractProjectReader
{
   @Override public ProjectFile read(String fileName) throws MPXJException
   {
      return read(new File(fileName));
   }

   @Override public List<ProjectFile> readAll(String fileName) throws MPXJException
   {
      return readAll(new File(fileName));
   }

   @Override public ProjectFile read(File file) throws MPXJException
   {
      m_readAll = false;
      List<ProjectFile> projects = readInternal(file);
      return projects.isEmpty() ? null : projects.get(0);
   }

   @Override public List<ProjectFile> readAll(File file) throws MPXJException
   {
      m_readAll = true;
      return readInternal(file);
   }

   private List<ProjectFile> readInternal(File file) throws MPXJException
   {
      try
      {
         List<ProjectFile> result;
         if (file.isDirectory())
         {
            result = handleDirectory(file);
         }
         else
         {
            result = handleFile(file);
         }
         return result;
      }
      catch (Exception ex)
      {
         throw new MPXJException(MPXJException.INVALID_FILE, ex);
      }
   }

   @Override public ProjectFile read(InputStream inputStream) throws MPXJException
   {
      m_readAll = false;
      List<ProjectFile> projects = readInternal(inputStream);
      return projects.isEmpty() ? null : projects.get(0);
   }

   @Override public List<ProjectFile> readAll(InputStream inputStream) throws MPXJException
   {
      m_readAll = true;
      return readInternal(inputStream);
   }

   private List<ProjectFile> readInternal(InputStream inputStream) throws MPXJException
   {
      try
      {
         BufferedInputStream bis = new BufferedInputStream(inputStream);
         bis.skip(m_skipBytes);
         bis.mark(BUFFER_SIZE);
         byte[] buffer = new byte[BUFFER_SIZE];
         int bytesRead = bis.read(buffer);
         bis.reset();

         //
         // If the file is smaller than the buffer we are peeking into,
         // it's probably not a valid schedule file.
         //
         if (bytesRead != BUFFER_SIZE)
         {
            return Collections.emptyList();
         }

         //
         // Always check for BOM first. Regex-based fingerprints may ignore these otherwise.
         //
         if (matchesFingerprint(buffer, UTF8_BOM_FINGERPRINT))
         {
            return handleByteOrderMark(bis, UTF8_BOM_FINGERPRINT.length, CharsetHelper.UTF8);
         }

         if (matchesFingerprint(buffer, UTF16_BOM_FINGERPRINT))
         {
            return handleByteOrderMark(bis, UTF16_BOM_FINGERPRINT.length, CharsetHelper.UTF16);
         }

         if (matchesFingerprint(buffer, UTF16LE_BOM_FINGERPRINT))
         {
            return handleByteOrderMark(bis, UTF16LE_BOM_FINGERPRINT.length, CharsetHelper.UTF16LE);
         }

         //
         // Now check for file fingerprints
         //
         if (matchesFingerprint(buffer, BINARY_PLIST))
         {
            return handleBinaryPropertyList(bis);
         }

         if (matchesFingerprint(buffer, OLE_COMPOUND_DOC_FINGERPRINT))
         {
            return handleOleCompoundDocument(bis);
         }

         if (matchesFingerprint(buffer, MSPDI_FINGERPRINT_1) || matchesFingerprint(buffer, MSPDI_FINGERPRINT_2) || matchesFingerprint(buffer, MSPDI_FINGERPRINT_3))
         {
            return readProjectFile(new MSPDIReader(), bis);
         }

         if (matchesFingerprint(buffer, PP_FINGERPRINT))
         {
            return readProjectFile(new AstaFileReader(), bis);
         }

         if (matchesFingerprint(buffer, MPX_FINGERPRINT))
         {
            return readProjectFile(new MPXReader(), bis);
         }

         if (matchesFingerprint(buffer, XER_FINGERPRINT))
         {
            return readProjectFile(new PrimaveraXERFileReader(), bis);
         }

         if (matchesFingerprint(buffer, PLANNER_FINGERPRINT))
         {
            return readProjectFile(new PlannerReader(), bis);
         }

         if (matchesFingerprint(buffer, PMXML_FINGERPRINT))
         {
            return readProjectFile(new PrimaveraPMFileReader(), bis);
         }

         if (matchesFingerprint(buffer, MDB_FINGERPRINT))
         {
            return handleMDBFile(bis);
         }

         if (matchesFingerprint(buffer, SQLITE_FINGERPRINT))
         {
            return handleSQLiteFile(bis);
         }

         if (matchesFingerprint(buffer, ZIP_FINGERPRINT))
         {
            return handleZipFile(bis);
         }

         if (matchesFingerprint(buffer, PHOENIX_FINGERPRINT))
         {
            return readProjectFile(new PhoenixReader(), new PhoenixInputStream(bis));
         }

         if (matchesFingerprint(buffer, PHOENIX_XML_FINGERPRINT))
         {
            return readProjectFile(new PhoenixReader(), bis);
         }

         if (matchesFingerprint(buffer, FASTTRACK_FINGERPRINT))
         {
            return readProjectFile(new FastTrackReader(), bis);
         }

         if (matchesFingerprint(buffer, PROJECTLIBRE_FINGERPRINT))
         {
            return readProjectFile(new ProjectLibreReader(), bis);
         }

         if (matchesFingerprint(buffer, GANTTPROJECT_FINGERPRINT))
         {
            return readProjectFile(new GanttProjectReader(), bis);
         }

         if (matchesFingerprint(buffer, TURBOPROJECT_FINGERPRINT))
         {
            return readProjectFile(new TurboProjectReader(), bis);
         }

         if (matchesFingerprint(buffer, DOS_EXE_FINGERPRINT))
         {
            return handleDosExeFile(bis);
         }

         if (matchesFingerprint(buffer, CONCEPT_DRAW_FINGERPRINT))
         {
            return readProjectFile(new ConceptDrawProjectReader(), bis);
         }

         if (matchesFingerprint(buffer, SYNCHRO_FINGERPRINT))
         {
            return readProjectFile(new SynchroReader(), bis);
         }

         if (matchesFingerprint(buffer, GANTT_DESIGNER_FINGERPRINT))
         {
            return readProjectFile(new GanttDesignerReader(), bis);
         }

         if (matchesFingerprint(buffer, SDEF_FINGERPRINT))
         {
            return readProjectFile(new SDEFReader(), bis);
         }

         if (matchesFingerprint(buffer, SCHEDULE_GRID_FINGERPRINT))
         {
            return readProjectFile(new SageReader(), bis);
         }

         if (matchesFingerprint(buffer, PROJECT_COMMANDER_FINGERPRINT_1) || matchesFingerprint(buffer, PROJECT_COMMANDER_FINGERPRINT_2))
         {
            return readProjectFile(new ProjectCommanderReader(), bis);
         }

         return Collections.emptyList();
      }

      catch (Exception ex)
      {
         throw new MPXJException(MPXJException.INVALID_FILE, ex);
      }
   }

   /**
    * Determine if the start of the buffer matches a fingerprint byte array.
    *
    * @param buffer bytes from file
    * @param fingerprint fingerprint bytes
    * @return true if the file matches the fingerprint
    */
   private boolean matchesFingerprint(byte[] buffer, byte[] fingerprint)
   {
      return Arrays.equals(fingerprint, Arrays.copyOf(buffer, fingerprint.length));
   }

   /**
    * Determine if the buffer, when expressed as text, matches a fingerprint regular expression.
    *
    * @param buffer bytes from file
    * @param fingerprint fingerprint regular expression
    * @return true if the file matches the fingerprint
    */
   private boolean matchesFingerprint(byte[] buffer, Pattern fingerprint)
   {
      return fingerprint.matcher(m_charset == null ? new String(buffer) : new String(buffer, m_charset)).matches();
   }

   /**
    * Adds listeners and reads from a stream.
    *
    * @param reader reader for file type
    * @param stream schedule data
    * @return ProjectFile instance
    */
   private List<ProjectFile> readProjectFile(ProjectReader reader, InputStream stream) throws MPXJException
   {
      addListenersToReader(reader);
      reader.setCharset(m_charset);
      return m_readAll ? reader.readAll(stream) : Arrays.asList(reader.read(stream));
   }

   /**
    * Adds listeners and reads from a file.
    *
    * @param reader reader for file type
    * @param file schedule data
    * @return ProjectFile instance
    */
   private List<ProjectFile> readProjectFile(ProjectReader reader, File file) throws MPXJException
   {
      addListenersToReader(reader);
      reader.setCharset(m_charset);
      return m_readAll ? reader.readAll(file) : Arrays.asList(reader.read(file));
   }

   /**
    * We have an OLE compound document... but is it an MPP file?
    *
    * @param stream file input stream
    * @return ProjectFile instance
    */
   private List<ProjectFile> handleOleCompoundDocument(InputStream stream) throws Exception
   {
      POIFSFileSystem fs;

      try
      {
         fs = new POIFSFileSystem(new CloseIgnoringInputStream(stream));
      }

      catch (Exception ex)
      {
         return Collections.emptyList();
      }

      String fileFormat = MPPReader.getFileFormat(fs);
      if (fileFormat != null && fileFormat.startsWith("MSProject"))
      {
         MPPReader reader = new MPPReader();
         addListenersToReader(reader);
         return Arrays.asList(reader.read(fs));
      }
      return Collections.emptyList();
   }

   /**
    * We have a binary property list.
    *
    * @param stream file input stream
    * @return ProjectFile instance
    */
   private List<ProjectFile> handleBinaryPropertyList(InputStream stream) throws Exception
   {
      // This is an unusual case. I have seen an instance where an MSPDI file was downloaded
      // as a web archive, which is a binary property list containing the file data.
      // This confused the UniversalProjectReader as it found a valid MSPDI fingerprint
      // but the binary plist header caused the XML parser to fail.
      // I'm not inclined to add support for extracting files from binary plists at the moment,
      // so adding this fingerprint allows us to cleanly reject the file as unsupported
      // rather than getting a confusing error from one of the other file type readers.
      return Collections.emptyList();
   }

   /**
    * We have identified that we have an MDB file. This could be a Microsoft Project database
    * or an Asta database. Open the database and use the table names present to determine
    * which type this is.
    *
    * @param stream schedule data
    * @return ProjectFile instance
    */
   private List<ProjectFile> handleMDBFile(InputStream stream) throws Exception
   {
      File file = InputStreamHelper.writeStreamToTempFile(stream, ".mdb");

      try
      {
         Class.forName("sun.jdbc.odbc.JdbcOdbcDriver");
         String url = "jdbc:odbc:DRIVER=Microsoft Access Driver (*.mdb);DBQ=" + file.getCanonicalPath();
         Set<String> tableNames = populateTableNames(url);

         if (tableNames.contains("MSP_PROJECTS"))
         {
            return readProjectFile(new MPDDatabaseReader(), file);
         }

         if (tableNames.contains("EXCEPTIONN"))
         {
            return readProjectFile(new AstaDatabaseReader(), file);
         }

         return Collections.emptyList();
      }

      finally
      {
         FileHelper.deleteQuietly(file);
      }
   }

   /**
    * We have identified that we have a SQLite file. This could be a Primavera Project database
    * or an Asta database. Open the database and use the table names present to determine
    * which type this is.
    *
    * @param stream schedule data
    * @return ProjectFile instance
    */
   private List<ProjectFile> handleSQLiteFile(InputStream stream) throws Exception
   {
      File file = InputStreamHelper.writeStreamToTempFile(stream, ".sqlite");

      try
      {
         Class.forName("org.sqlite.JDBC");
         String url = "jdbc:sqlite:" + file.getCanonicalPath();
         Set<String> tableNames = populateTableNames(url);

         if (tableNames.contains("EXCEPTIONN"))
         {
            return readProjectFile(new AstaDatabaseFileReader(), file);
         }

         if (tableNames.contains("PROJWBS"))
         {
            return readProjectFile(new PrimaveraDatabaseFileReader(), file);
         }

         if (tableNames.contains("ZSCHEDULEITEM"))
         {
            return readProjectFile(new MerlinReader(), file);
         }

         return Collections.emptyList();
      }

      finally
      {
         FileHelper.deleteQuietly(file);
      }
   }

   /**
    * We have identified that we have a zip file. Extract the contents into
    * a temporary directory and process.
    *
    * @param stream schedule data
    * @return ProjectFile instance
    */
   private List<ProjectFile> handleZipFile(InputStream stream) throws Exception
   {
      File dir = null;

      try
      {
         dir = InputStreamHelper.writeZipStreamToTempDir(stream);
         return handleDirectory(dir);
      }

      finally
      {
         FileHelper.deleteQuietly(dir);
      }
   }

   /**
    * Open and read a file.
    *
    * @param file File instance
    * @return ProjectFile instance
    */
   private List<ProjectFile> handleFile(File file) throws Exception
   {
      FileInputStream fis = null;

      try
      {
         fis = new FileInputStream(file);
         List<ProjectFile> projectFiles = readInternal(fis);
         fis.close();
         return projectFiles;
      }

      finally
      {
         AutoCloseableHelper.closeQuietly(fis);
      }
   }

   /**
    * We have a directory. Determine if this contains a multi-file database we understand, if so
    * process it. If it does not contain a database, test each file within the directory
    * structure to determine if it contains a file whose format we understand.
    *
    * @param directory directory to process
    * @return ProjectFile instance if we can process anything, or null
    */
   private List<ProjectFile> handleDirectory(File directory) throws Exception
   {
      List<ProjectFile> result = handleDatabaseInDirectory(directory);
      if (result.isEmpty())
      {
         result = handleFileInDirectory(directory);
      }
      return result;
   }

   /**
    * Given a directory, determine if it contains a multi-file database whose format
    * we can process.
    *
    * @param directory directory to process
    * @return ProjectFile instance if we can process anything, or null
    */
   private List<ProjectFile> handleDatabaseInDirectory(File directory) throws Exception
   {
      byte[] buffer = new byte[BUFFER_SIZE];
      File[] files = directory.listFiles();
      if (files != null)
      {
         for (File file : files)
         {
            if (file.isDirectory())
            {
               continue;
            }

            FileInputStream fis = new FileInputStream(file);
            int bytesRead = fis.read(buffer);
            fis.close();

            //
            // If the file is smaller than the buffer we are peeking into,
            // it's probably not a valid schedule file.
            //
            if (bytesRead != BUFFER_SIZE)
            {
               continue;
            }

            if (matchesFingerprint(buffer, BTRIEVE_FINGERPRINT))
            {
               return handleP3BtrieveDatabase(directory);
            }

            if (matchesFingerprint(buffer, STW_FINGERPRINT))
            {
               return handleSureTrakDatabase(directory);
            }
         }
      }
      return Collections.emptyList();
   }

   /**
    * Given a directory, determine if it  (or any subdirectory) contains a file
    * whose format we understand.
    *
    * @param directory directory to process
    * @return ProjectFile instance if we can process anything, or null
    */
   private List<ProjectFile> handleFileInDirectory(File directory) throws Exception
   {
      List<File> directories = new ArrayList<>();
      File[] files = directory.listFiles();

      if (files != null)
      {
         // Try files first
         for (File file : files)
         {
            if (file.isDirectory())
            {
               directories.add(file);
            }
            else
            {
               UniversalProjectReader reader = new UniversalProjectReader();
               if (m_readAll)
               {
                  List<ProjectFile> result = reader.readAll(file);
                  if (!result.isEmpty())
                  {
                     return result;
                  }
               }
               else
               {
                  ProjectFile result = reader.read(file);
                  if (result != null)
                  {
                     return Arrays.asList(result);
                  }
               }
            }
         }

         // Haven't found a file we can read? Try the directories.
         for (File file : directories)
         {
            List<ProjectFile> result = handleDirectory(file);
            if (!result.isEmpty())
            {
               return result;
            }
         }
      }
      return Collections.emptyList();
   }

   /**
    * Determine if we have a P3 Btrieve multi-file database.
    *
    * @param directory directory to process
    * @return ProjectFile instance if we can process anything, or null
    */
   private List<ProjectFile> handleP3BtrieveDatabase(File directory) throws Exception
   {
      return m_readAll ? new P3DatabaseReader().readAll(directory) : Arrays.asList(P3DatabaseReader.setProjectNameAndRead(directory));
   }

   /**
    * Determine if we have a SureTrak multi-file database.
    *
    * @param directory directory to process
    * @return ProjectFile instance if we can process anything, or null
    */
   private List<ProjectFile> handleSureTrakDatabase(File directory) throws Exception
   {
      return m_readAll ? new SureTrakDatabaseReader().readAll(directory) : Arrays.asList(SureTrakDatabaseReader.setProjectNameAndRead(directory));
   }

   /**
    * The file we are working with has a byte order mark. Skip this and try again to read the file.
    *
    * @param stream schedule data
    * @param length length of the byte order mark
    * @param charset charset indicated by byte order mark
    * @return ProjectFile instance
    */
   private List<ProjectFile> handleByteOrderMark(InputStream stream, int length, Charset charset) throws Exception
   {
      UniversalProjectReader reader = new UniversalProjectReader();
      reader.m_skipBytes = length;
      reader.m_charset = charset;
      return m_readAll ? reader.readAll(stream) : Arrays.asList(reader.read(stream));
   }

   /**
    * This could be a self-extracting archive. If we understand the format, expand
    * it and check the content for files we can read.
    *
    * @param stream schedule data
    * @return ProjectFile instance
    */
   private List<ProjectFile> handleDosExeFile(InputStream stream) throws Exception
   {
      File file = InputStreamHelper.writeStreamToTempFile(stream, ".tmp");
      InputStream is = null;

      try
      {
         is = new FileInputStream(file);
         if (is.available() > 1350)
         {
            StreamHelper.skip(is, 1024);

            // Bytes at offset 1024
            byte[] data = new byte[2];
            is.read(data);

            if (matchesFingerprint(data, WINDOWS_NE_EXE_FINGERPRINT))
            {
               StreamHelper.skip(is, 286);

               // Bytes at offset 1312
               data = new byte[34];
               is.read(data);
               if (matchesFingerprint(data, PRX_FINGERPRINT))
               {
                  is.close();
                  is = null;
                  return readProjectFile(new P3PRXFileReader(), file);
               }
            }

            if (matchesFingerprint(data, STX_FINGERPRINT))
            {
               StreamHelper.skip(is, 31742);
               // Bytes at offset 32768
               data = new byte[4];
               is.read(data);
               if (matchesFingerprint(data, PRX3_FINGERPRINT))
               {
                  is.close();
                  is = null;
                  return readProjectFile(new SureTrakSTXFileReader(), file);
               }
            }
         }
         return Collections.emptyList();
      }

      finally
      {
         AutoCloseableHelper.closeQuietly(is);
         FileHelper.deleteQuietly(file);
      }
   }

   /**
    * Open a database and build a set of table names.
    *
    * @param url database URL
    * @return set containing table names
    */
   private Set<String> populateTableNames(String url) throws SQLException
   {
      Set<String> tableNames = new HashSet<>();
      Connection connection = null;
      ResultSet rs = null;

      try
      {
         connection = DriverManager.getConnection(url);
         DatabaseMetaData dmd = connection.getMetaData();
         rs = dmd.getTables(null, null, null, null);
         while (rs.next())
         {
            tableNames.add(rs.getString("TABLE_NAME").toUpperCase());
         }
      }

      finally
      {
         AutoCloseableHelper.closeQuietly(rs);
         AutoCloseableHelper.closeQuietly(connection);
      }

      return tableNames;
   }

   private int m_skipBytes;
   private Charset m_charset;
   private boolean m_readAll;

   private static final int BUFFER_SIZE = 512;

   private static final byte[] OLE_COMPOUND_DOC_FINGERPRINT =
   {
      (byte) 0xD0,
      (byte) 0xCF,
      (byte) 0x11,
      (byte) 0xE0,
      (byte) 0xA1,
      (byte) 0xB1,
      (byte) 0x1A,
      (byte) 0xE1
   };

   private static final byte[] PP_FINGERPRINT =
   {
      (byte) 0x00,
      (byte) 0x00,
      (byte) 0x30,
      (byte) 0x30,
      (byte) 0x30,
      (byte) 0x30,
      (byte) 0x30,
      (byte) 0x30
   };

   private static final byte[] MPX_FINGERPRINT =
   {
      (byte) 'M',
      (byte) 'P',
      (byte) 'X'
   };

   private static final byte[] MDB_FINGERPRINT =
   {
      (byte) 0x00,
      (byte) 0x01,
      (byte) 0x00,
      (byte) 0x00,
      (byte) 'S',
      (byte) 't',
      (byte) 'a',
      (byte) 'n',
      (byte) 'd',
      (byte) 'a',
      (byte) 'r',
      (byte) 'd',
      (byte) ' ',
      (byte) 'J',
      (byte) 'e',
      (byte) 't',
      (byte) ' ',
      (byte) 'D',
      (byte) 'B',
   };

   private static final byte[] SQLITE_FINGERPRINT =
   {
      (byte) 'S',
      (byte) 'Q',
      (byte) 'L',
      (byte) 'i',
      (byte) 't',
      (byte) 'e',
      (byte) ' ',
      (byte) 'f',
      (byte) 'o',
      (byte) 'r',
      (byte) 'm',
      (byte) 'a',
      (byte) 't'
   };

   private static final byte[] ZIP_FINGERPRINT =
   {
      (byte) 'P',
      (byte) 'K'
   };

   private static final byte[] PHOENIX_FINGERPRINT =
   {
      (byte) 'P',
      (byte) 'P',
      (byte) 'X',
      (byte) '!',
      (byte) '!',
      (byte) '!',
      (byte) '!'
   };

   private static final byte[] BINARY_PLIST =
   {
      (byte) 'b',
      (byte) 'p',
      (byte) 'l',
      (byte) 'i',
      (byte) 's',
      (byte) 't'
   };

   private static final byte[] FASTTRACK_FINGERPRINT =
   {
      (byte) 0x1C,
      (byte) 0x00,
      (byte) 0x00,
      (byte) 0x00,
   };

   private static final byte[] PROJECTLIBRE_FINGERPRINT =
   {
      (byte) 0xAC,
      (byte) 0xED,
      (byte) 0x00,
      (byte) 0x05
   };

   private static final byte[] BTRIEVE_FINGERPRINT =
   {
      (byte) 0x46,
      (byte) 0x43,
      (byte) 0x00,
      (byte) 0x00
   };

   private static final byte[] STW_FINGERPRINT =
   {
      (byte) 0x53,
      (byte) 0x54,
      (byte) 0x57
   };

   private static final byte[] DOS_EXE_FINGERPRINT =
   {
      (byte) 0x4D,
      (byte) 0x5A
   };

   private static final byte[] WINDOWS_NE_EXE_FINGERPRINT =
   {
      (byte) 0x4E,
      (byte) 0x45
   };

   private static final byte[] STX_FINGERPRINT =
   {
      (byte) 0x55,
      (byte) 0x8B
   };

   private static final byte[] SYNCHRO_FINGERPRINT =
   {
      (byte) 0xB6,
      (byte) 0x17
   };

   private static final byte[] SDEF_FINGERPRINT =
   {
      (byte) 'V',
      (byte) 'O',
      (byte) 'L',
      (byte) 'M'
   };

   private static final byte[] SCHEDULE_GRID_FINGERPRINT =
   {
      (byte) '*',
      (byte) '*',
      (byte) '*',
      (byte) '*',
      (byte) ' ',
      (byte) 'S',
      (byte) 'c',
      (byte) 'h',
      (byte) 'e',
      (byte) 'd',
      (byte) 'u',
      (byte) 'l',
      (byte) 'e',
      (byte) ' ',
      (byte) 'G',
      (byte) 'r',
      (byte) 'i',
      (byte) 'd'
   };

   private static final byte[] UTF8_BOM_FINGERPRINT =
   {
      (byte) 0xEF,
      (byte) 0xBB,
      (byte) 0xBF
   };

   private static final byte[] UTF16_BOM_FINGERPRINT =
   {
      (byte) 0xFE,
      (byte) 0xFF
   };

   private static final byte[] UTF16LE_BOM_FINGERPRINT =
   {
      (byte) 0xFF,
      (byte) 0xFE
   };

   private static final byte[] PROJECT_COMMANDER_FINGERPRINT_1 =
   {
      (byte) 0x00,
      (byte) 0x80,
      (byte) 0x01,
      (byte) 0x00
   };

   private static final byte[] PROJECT_COMMANDER_FINGERPRINT_2 =
   {
      (byte) 0x02,
      (byte) 0x80,
      (byte) 0x01,
      (byte) 0x00
   };

   private static final Pattern XER_FINGERPRINT = Pattern.compile("ERMHDR.*", Pattern.DOTALL);

   private static final Pattern PLANNER_FINGERPRINT = Pattern.compile(".*<project.*mrproject-version.*", Pattern.DOTALL);

   private static final Pattern PMXML_FINGERPRINT = Pattern.compile(".*(<BusinessObjects|APIBusinessObjects).*", Pattern.DOTALL);

   private static final Pattern MSPDI_FINGERPRINT_1 = Pattern.compile(".*xmlns=\"http://schemas\\.microsoft\\.com/project.*", Pattern.DOTALL);

   private static final Pattern MSPDI_FINGERPRINT_2 = Pattern.compile(".*<Project.*<SaveVersion>.*", Pattern.DOTALL);

   private static final Pattern MSPDI_FINGERPRINT_3 = Pattern.compile(".*<Project.*<Title>.*", Pattern.DOTALL);

   private static final Pattern PHOENIX_XML_FINGERPRINT = Pattern.compile(".*<project.*version=\"(\\d+|\\d+\\.\\d+)\".*update_mode=\"(true|false)\".*>.*", Pattern.DOTALL);

   private static final Pattern GANTTPROJECT_FINGERPRINT = Pattern.compile(".*<project.*webLink.*", Pattern.DOTALL);

   private static final Pattern TURBOPROJECT_FINGERPRINT = Pattern.compile(".*dWBSTAB.*", Pattern.DOTALL);

   private static final Pattern PRX_FINGERPRINT = Pattern.compile("!Self-Extracting Primavera Project", Pattern.DOTALL);

   private static final Pattern PRX3_FINGERPRINT = Pattern.compile("PRX3", Pattern.DOTALL);

   private static final Pattern CONCEPT_DRAW_FINGERPRINT = Pattern.compile(".*Application=\\\"CDProject\\\".*", Pattern.DOTALL);

   private static final Pattern GANTT_DESIGNER_FINGERPRINT = Pattern.compile(".*<Gantt Version=.*", Pattern.DOTALL);
}
