packages feed

sequence-formats-1.12.1.0: src/SequenceFormats/Utils.hs

{-# LANGUAGE OverloadedStrings #-}

-- |This module contains helper functions for file parsing.

module SequenceFormats.Utils (liftParsingErrors,
                              consumeProducer, readFileProd, readFileProdCheckCompress,
                              decompressMultiMember,
                              SeqFormatException(..), deflateFinaliser,
                              Chrom(..), word, gzipConsumer, writeFromPopper, Z.Deflate) where

import           Control.Error                    (readErr)
import           Control.Exception                (Exception, throw, throwIO)
import           Control.Monad                    (unless)
import           Control.Monad.Catch              (MonadThrow, throwM)
import           Control.Monad.IO.Class           (MonadIO, liftIO)
import           Control.Monad.Trans.Class        (lift)
import qualified Data.Attoparsec.ByteString.Char8 as A
import qualified Data.ByteString.Char8            as B
import           Data.Char                        (isSpace)
import           Data.List                        (isSuffixOf)
import qualified Data.Streaming.Zlib              as Z
import           Pipes                            (Consumer, Producer, await,
                                                   next, yield)
import           Pipes.Attoparsec                 (ParsingError (..), parsed)
import qualified Pipes.ByteString                 as PB
import qualified Pipes.Safe                       as PS
import qualified Pipes.Safe.Prelude               as PS
import           System.IO                        (Handle, IOMode (..))

-- |An exception type for parsing BioInformatic file formats.
data SeqFormatException = SeqFormatException String
    deriving (Eq)

instance Show SeqFormatException where
    show (SeqFormatException msg) = "SeqFormatException: " ++ msg

instance Exception SeqFormatException

-- |A wrapper datatype for Chromosome names.
newtype Chrom = Chrom {unChrom :: B.ByteString} deriving (Eq)

-- |Show instance for Chrom
instance Show Chrom where
    show (Chrom c) = B.unpack c

-- |Ord instance for Chrom
instance Ord Chrom where
    compare (Chrom c1) (Chrom c2) =
        let (c1NoChr, c2NoChr) = (removeChr c1, removeChr c2)
            (c1XYMTconvert, c2XYMTconvert) = (convertXYMT c1NoChr, convertXYMT c2NoChr)
        in  case (,) <$> readChrom c1XYMTconvert <*> readChrom c2XYMTconvert of
                Left e           -> throw e
                Right (cn1, cn2) -> cn1 `compare` cn2
      where
        removeChr :: B.ByteString -> B.ByteString
        removeChr c = if B.take 3 c == "chr" then B.drop 3 c else c
        convertXYMT :: B.ByteString -> B.ByteString
        convertXYMT c = case c of
            "X"  -> "23"
            "Y"  -> "24"
            "MT" -> "90"
            n    -> n
        readChrom :: B.ByteString -> Either SeqFormatException Int
        readChrom c = readErr (SeqFormatException $ "cannot parse chromosome " ++ B.unpack c) . B.unpack $ c

-- |A function to help with reporting parsing errors to stderr. Returns a clean Producer over the
-- parsed datatype.
liftParsingErrors :: (MonadThrow m) =>
    Either (ParsingError, Producer B.ByteString m r) () -> Producer a m ()
liftParsingErrors res = case res of
    Left (ParsingError _ msg, restProd) -> do
        x <- lift $ next restProd
        case x of
            Right (chunk, _) -> do
                let firstLine = B.unpack . B.takeWhile (/= '\n') $ chunk
                    msg' = "Error while parsing: " <> msg <> ". Offending line: " ++ firstLine
                throwM $ SeqFormatException msg'
            Left _ -> error "should not happen"
    Right () -> return ()

-- |A helper function to parse a text producer, properly reporting all errors to stderr.
consumeProducer :: (MonadThrow m) => A.Parser a -> Producer B.ByteString m () -> Producer a m ()
consumeProducer parser prod = parsed parser prod >>= liftParsingErrors

readFileProd :: (PS.MonadSafe m) => FilePath -> Producer B.ByteString m ()
readFileProd f = PS.withFile f ReadMode PB.fromHandle

readFileProdCheckCompress :: (PS.MonadSafe m) => FilePath -> Producer B.ByteString m ()
readFileProdCheckCompress f =
    let decompressFunc = if ".gz" `isSuffixOf` f then decompressGzip ("gzip file " ++ f) else id
    in  decompressFunc $ PS.withFile f ReadMode PB.fromHandle

-- |Decompresses a gzip stream that may consist of multiple concatenated gzip members, as is the
-- case for BGZF files written by bgzip, bcftools or GATK. Pipes.GZip.decompress on its own stops
-- after the first member and silently drops the rest of the input. Throws a SeqFormatException
-- if the input is not valid gzip data, or if it ends in the middle of a gzip member (e.g. a
-- truncated file), which Pipes.GZip.decompress would silently accept.
decompressMultiMember :: (MonadIO m) => Producer B.ByteString m r -> Producer B.ByteString m r
decompressMultiMember = decompressGzip "gzip stream"

-- |Like decompressMultiMember, but takes a description of the input (e.g. the file name) for
-- error messages.
decompressGzip :: (MonadIO m) => String -> Producer B.ByteString m r -> Producer B.ByteString m r
decompressGzip descr = newMember True
  where
    newMember isFirst prod = liftIO (Z.initInflate (Z.WindowBits 31)) >>= go isFirst False prod
    -- hasInput tracks whether the current member has received any bytes yet, so that we can
    -- tell a clean end of input (after a completed member) from a truncated member.
    go isFirst hasInput prod inf = do
        res <- lift (next prod)
        case res of
            Left r -> do
                complete <- liftIO (Z.isCompleteInflate inf)
                if complete || (not hasInput && not isFirst) then return r else
                    liftIO . throwIO . SeqFormatException $ if hasInput
                        then descr ++ " ended unexpectedly. The file seems to be truncated"
                        else descr ++ " is empty, which is not valid gzip"
            Right (bs, prod')
                | B.null bs -> go isFirst hasInput prod' inf
                | otherwise -> do
                    popper <- liftIO (Z.feedInflate inf bs)
                    yieldPopper popper
                    rest <- liftIO (Z.flushInflate inf)
                    unless (B.null rest) (yield rest)
                    complete <- liftIO (Z.isCompleteInflate inf)
                    if complete then do
                        leftover <- liftIO (Z.getUnusedInflate inf)
                        newMember False (yield leftover >> prod')
                    else
                        go isFirst True prod' inf
    yieldPopper popper = do
        popRes <- liftIO popper
        case popRes of
            Z.PRDone -> return ()
            Z.PRNext bs -> yield bs >> yieldPopper popper
            Z.PRError (Z.ZlibException code) -> liftIO . throwIO . SeqFormatException $
                "could not decompress " ++ descr ++ " (zlib error code " ++ show code ++
                    "). The file seems to be corrupt or not gzip-compressed"

word :: A.Parser B.ByteString
word = A.takeTill isSpace

gzipConsumer :: (MonadIO m) => Z.Deflate -> Handle -> Consumer B.ByteString m ()
gzipConsumer def h = do
    bs <- await
    pop <- liftIO (Z.feedDeflate def bs)
    liftIO (writeFromPopper pop h)
    gzipConsumer def h

writeFromPopper :: (MonadIO m) => Z.Popper -> Handle -> m ()
writeFromPopper pop h = do
   popResult <- liftIO pop
   case popResult of
      Z.PRDone    -> return ()
      Z.PRError e -> liftIO $ throwIO e
      Z.PRNext bs -> do
         liftIO $ B.hPut h bs
         writeFromPopper pop h

deflateFinaliser :: (MonadIO m) => Z.Deflate -> Handle -> m ()
deflateFinaliser def h = do
    let finalPop = liftIO $ Z.finishDeflate def
    writeFromPopper finalPop h