packages feed

DPM-0.2.1: src/DPM/Core/Storage.hs

{-# LANGUAGE ScopedTypeVariables #-}
-- NB: clients of this module must perform locking on their own.
module DPM.Core.Storage (

  Comment, PatchesResult(..), PatchBundleName,

  withLock, setupStorageDir,

  addPatchBundle, addPatch, markAsReviewed, markAsRejected, markAsObsolete,
  markAsObsoleteNoCheck, markAsUndecided, markAsApplied, applyPatch,
  closePatchGroup, openPatchGroup, getPatches, getPatchFile, allPatchIDs,
  allConflicts, addComment

) where

import Prelude hiding ( catch )
import qualified Data.Map as Map
import System.FilePath
import qualified Data.ByteString as B
import qualified Darcs.Lock
import Control.Monad
import Control.Monad.Trans
import System.Directory ( doesFileExist, createDirectoryIfMissing )
import Control.Exception
import Data.IORef
import Data.Time ( getCurrentTime )
import Data.Maybe ( mapMaybe )
import Data.Char ( toLower )
import qualified Data.List as List
import System.IO
import System.IO.Unsafe
import Data.IORef

import DPM.Core.DataTypes
import qualified DPM.Core.Model as M
import DPM.Core.Utils
import DPM.Core.DPM_Monad
import DPM.Core.Conflicts
import DPM.Core.ReverseDependencies

type Patches = Map.Map PatchID (Patch, PatchBundleName)
type Log a = Map.Map a [LogEntry]
type Comment = String

newtype PatchBundleName = PatchBundleName { unPatchBundleName :: String }
                        deriving (Show,Read)

newtype DPMStorageVersion = DPMStorageVersion { unVersion :: Int }
                deriving (Show,Read,Eq,Ord)

ppVersion :: DPMStorageVersion -> String
ppVersion (DPMStorageVersion i) = show i

currentVersion :: DPMStorageVersion
currentVersion = DPMStorageVersion 1

withLock :: DPM a -> DPM a
withLock x =
    do fname <- getDPMConfigValue cfg_lockFile
       io <- asIO x
       liftIO $
         do -- withLock terminates the program, so we have to
            -- workaround this fact
            ref <- newIORef False
            (Darcs.Lock.withLock fname (writeIORef ref True >> io))
              `catch` (\ (e::SomeException) ->
                           do b <- readIORef ref
                              if b then throwIO e
                                   else failIO ("Could not obtain lock "
                                                ++ show fname ++
                                               ", aborting."))

setupStorageDir :: DPM ()
setupStorageDir =
    do cfg <- getDPMConfig
       liftIO $ createDirectoryIfMissing False (cfg_dataDir cfg)
       liftIO $ createDirectoryIfMissing False (cfg_reviewDir cfg)
       writeIfNonExisting (cfg_modelFile cfg) M.emptyModel
       writeIfNonExisting (cfg_patchesFile cfg) (Map.empty :: Patches)
       writeIfNonExisting (cfg_patchLog cfg) (Map.empty :: Log PatchID)
       writeIfNonExisting (cfg_patchGroupLog cfg) (Map.empty ::
                                                   Log PatchGroupID)
    where
      writeIfNonExisting :: Show a => FilePath -> a -> DPM ()
      writeIfNonExisting fname x =
          do exists <- liftIO (doesFileExist fname)
             unless exists (writeToFile fname x)

readFromFile :: Read a => FilePath -> DPM a
readFromFile fname =
    do debugDPM ("Storage: reading " ++ fname)
       s <- liftIO $ readFile fname
       liftIO $ evaluate (length s) -- force the string!
       case reads s of
         [(version, rest)] ->
             if version /= currentVersion
                then fail ("Version of storage is " ++ ppVersion version ++
                           " but DPM can handle only storage version " ++
                           ppVersion currentVersion ++
                           ". Migrate your storage if possible.")
                else case readM rest of
                       Nothing -> fail ("Content of " ++ fname ++ " corrupted")
                       Just m -> return m
         _ -> fail ("Content of " ++ fname ++ " corrupted")

writeToFile :: Show a => FilePath -> a -> DPM ()
writeToFile fname x =
    do debugDPM ("Storage: writing " ++ fname)
       liftIO $ writeFile fname (show currentVersion ++ show x)

modifyFile :: (Show a, Read a) => FilePath -> (a -> DPM a) -> DPM a
modifyFile fname trans =
    do x <- readFromFile fname
       y <- trans x
       writeToFile fname y
       return y

readModel :: DPM M.Model
readModel =
    do fname <- getDPMConfigValue cfg_modelFile
       readFromFile fname

writeModel :: M.Model -> DPM ()
writeModel model =
    do fname <- getDPMConfigValue cfg_modelFile
       writeToFile fname model

readPatches :: DPM Patches
readPatches =
    do fname <- getDPMConfigValue cfg_patchesFile
       readFromFile fname

writePatches :: Patches -> DPM ()
writePatches patches =
    do fname <- getDPMConfigValue cfg_patchesFile
       writeToFile fname patches

insertPatch :: Patch -> PatchBundleName -> Patches -> Patches
insertPatch p bname ps = Map.insert (p_id p) (p, bname) ps

lookupPatch :: PatchID -> Patches -> Maybe Patch
lookupPatch pid patches =
    do (p,_) <- Map.lookup pid patches
       return p

getPatchFile :: PatchID -> DPM FilePath
getPatchFile pid =
    do patches <- readPatches
       getPatchFileIntern patches pid

getPatchFileIntern :: Patches -> PatchID -> DPM FilePath
getPatchFileIntern patches pid =
    do case Map.lookup pid patches of
         Nothing -> fail ("Internal state inconsistent: patch with ID " ++
                          show pid ++ " not found")
         Just (_, PatchBundleName name) ->
             do dir <- getDPMConfigValue cfg_dataDir
                return (dir </> name)

withModelAndPatchesLogPG :: (M.Model -> Patches -> (M.Partial M.Model, Patches,
                                                    PatchGroupID, String))
                         -> DPM Bool
withModelAndPatchesLogPG f =
    withModelAndPatchesGeneric cfg_patchGroupLog (\m p -> return (f m p))

withModelAndPatches :: (M.Model -> Patches -> (M.Partial M.Model, Patches,
                                               PatchID, String))
                    -> DPM Bool
withModelAndPatches f = withModelAndPatchesDPM (\m p -> return (f m p))

withModelAndPatchesDPM :: (M.Model -> Patches ->
                           DPM (M.Partial M.Model, Patches,
                                PatchID, String))
                       -> DPM Bool
withModelAndPatchesDPM = withModelAndPatchesGeneric cfg_patchLog

withModelAndPatchesGeneric :: (Show a, Read a, Ord a) =>
                              (DPMConfig -> FilePath)
                           -> (M.Model -> Patches ->
                               DPM (M.Partial M.Model, Patches, a, String))
                           -> DPM Bool
withModelAndPatchesGeneric logFileFun f =
    do model <- readModel
       patches <- readPatches
       res <- f model patches
       case res of
         (Left err, _, _, _) -> fail err
         (Right model', patches', a, logMsg) ->
             do writeModel model'
                writePatches patches'
                t <- liftIO $ getCurrentTime
                user <- getDPMConfigValue cfg_currentUser
                logFile <- getDPMConfigValue logFileFun
                let logEntry = LogEntry t user (model /= model') logMsg
                modifyFile logFile
                    (\ m -> case Map.lookup a m of
                              Nothing -> return $ Map.insert a [logEntry] m
                              Just l -> return $ Map.insert a (logEntry:l) m)
                return (model /= model')

addPatchBundle :: B.ByteString -> DPM PatchBundleName
addPatchBundle bs =
    do dir <- getDPMConfigValue cfg_dataDir
       liftIO $ do (tmpFile, handle) <- openTempFile dir ".dpatch"
                   B.hPutStr handle bs
                   hClose handle
                   return (PatchBundleName (takeFileName tmpFile))

addPatch :: Patch -> PatchBundleName -> [PatchID] -> Bool -> DPM Bool
addPatch p bundleName conflicts conflictWithRepo =
    withModelAndPatchesDPM $ \model patches ->
    case M.addPatch model (p_id p) (p_name p) (p_dependents p)
           conflicts conflictWithRepo
    of
      Left err -> return (Left err, patches, p_id p, "added")
      Right model' ->
          do return (Right model',
                     insertPatch p bundleName patches,
                     p_id p,
                     "added")

markAsReviewed :: PatchID -> Comment -> DPM Bool
markAsReviewed pid comment = withModelAndPatches $
    \model patches -> (M.markAsReviewed model pid, patches,
                       pid, "marked as reviewed: " ++ comment)

markAsRejected :: PatchID -> Comment -> DPM Bool
markAsRejected pid comment = withModelAndPatches $
    \model patches -> (M.markAsDiscarded model pid ReasonRejected, patches,
                       pid, "marked as rejected: " ++ comment)

markAsObsolete :: PatchID -> Comment -> DPM Bool
markAsObsolete pid comment = withModelAndPatches $
    \model patches -> (M.markAsDiscarded model pid ReasonObsolete, patches,
                       pid, "marked as obsolete: " ++ comment)

markAsObsoleteNoCheck :: PatchID -> Comment -> DPM Bool
markAsObsoleteNoCheck pid comment = withModelAndPatches $
    \model patches -> (M.markAsDiscarded' False model pid ReasonObsolete,
                       patches, pid, "marked as obsolete: " ++ comment)

markAsUndecided :: PatchID -> Comment -> DPM Bool
markAsUndecided pid comment = withModelAndPatches $
    \model patches -> (M.markAsUndecided model pid, patches,
                       pid, "marked as undecided: " ++ comment)

markAsApplied :: PatchID -> Comment -> DPM Bool
markAsApplied pid comment = withModelAndPatches $
    \model patches -> (M.markAsApplied model pid, patches,
                       pid, "marked as applied: " ++ comment)

addComment :: PatchID -> Comment -> DPM Bool
addComment pid comment = withModelAndPatches $
    \model patches -> (Right model, patches, pid, comment)

applyPatch :: PatchID
           -> (FilePath -> DPM ()) -- Applies the patch stored in the file
           -> Maybe Comment
           -> DPM Bool
applyPatch pid applyFun comment = withModelAndPatchesDPM $
    \model patches ->
        case M.markAsApplied model pid of
          Left err -> return (Left err, patches, pid, "applied")
          Right model' ->
              do when (model' /= model) $
                      do patchFile <- getPatchFileIntern patches pid
                         applyFun patchFile
                 return (Right model', patches, pid,
                         case comment of
                           Nothing -> "applied"
                           Just s -> "applied (" ++ s ++ ")")

closePatchGroup :: PatchGroupID -> DPM Bool
closePatchGroup pgid = withModelAndPatchesLogPG $
    \model patches -> (M.closePatchGroup model pgid, patches,
                       pgid, "closed patch group")

openPatchGroup :: PatchID -> DPM Bool
openPatchGroup pid = withModelAndPatchesLogPG $
    \model patches ->
        case M.openPatchGroup model pid of
          Left err -> (Left err, patches,
                       error "no patch group id", "opened patch group")
          Right (model', pgid) -> (Right model', patches,
                                   pgid, "opened patch group")

data PatchesResult = PatchesResult { pr_groups :: [PatchGroup (Patch, FilePath)]
                                   , pr_revDeps :: PatchRevDeps
                                   , pr_allPatchIDs :: [PatchID]
                                   , pr_allConflicts :: PatchConflicts }

-- FIXME: test filtering
getPatches :: Query -> DPM PatchesResult
getPatches query =
    do model <- readModel
       patches <- readPatches
       let allPatchIDs = Map.keys patches
           patches' = M.getPatches model
           revDeps = buildRevDeps (map (\(p,_) -> (p_id p, p_dependents p))
                                       (Map.elems patches))
       logFile <- getDPMConfigValue cfg_patchLog
       logMap <- readFromFile logFile
       allGroups <- mapM (convertPG logMap patches) patches'
       let matchingGroups = mapMaybe (filterPatchGroups revDeps) allGroups
       return $ PatchesResult matchingGroups revDeps
                              allPatchIDs (M.allConflicts model)
    where
      -- Conversion of patches and groups (injects additional info etc.)
      convertPG :: Log PatchID -> Patches -> PatchGroup (M.Patch PatchState)
                -> DPM (PatchGroup (Patch, FilePath))
      convertPG logMap allPatches (PatchGroup id state ps c) =
          do ps' <- mapM (retrievePatch logMap allPatches) ps
             return $ PatchGroup id state ps' c
      retrievePatch :: Log PatchID -> Patches -> M.Patch PatchState
                    -> DPM (Patch, FilePath)
      retrievePatch logMap patches mp =
          case lookupPatch (M.p_id mp) patches of
            Nothing -> fail ("Internal state inconsistent: patch with ID " ++
                             show (M.p_id mp) ++ " not found")
            Just p ->
                do let log = case Map.lookup (p_id p) logMap of
                               Nothing -> []
                               Just l -> l
                   patchFile <- getPatchFileIntern patches (p_id p)
                   return $
                     (p { p_state = M.p_state mp
                        , p_tags = if M.p_isReviewed mp then [TagReviewed]
                                                        else []
                        , p_log = log }
                     ,patchFile)
      -- Filtering
      filterPatchGroups :: PatchRevDeps -> PatchGroup (Patch, FilePath)
                        -> Maybe (PatchGroup (Patch, FilePath))
      filterPatchGroups revDeps pg =
          let patches = pg_patches pg
              patches' = filter (matchesPatch revDeps (pg_state pg)) patches
          in if null patches'
                then Nothing
                else Just $ pg { pg_patches = patches'
                               , pg_complete = (length patches ==
                                                length patches') }
      matchesPatch :: PatchRevDeps -> PatchGroupState
                   -> (Patch, FilePath) -> Bool
      matchesPatch revDeps pgState (p, _) =
          let strings = [prettyDate (p_date p),
                         unPatchGroupID (p_name p),
                         p_author p]
              trailingStrings = [unPatchID (p_id p)] ++
                                (map unPatchID (p_dependents p)) ++
                                (map unPatchID (getRevDeps revDeps (p_id p)))
          in evalQuery query pgState p
               (map (\s -> (s, False)) strings ++
                map (\s -> (s, True)) trailingStrings)
      evalQuery :: Query -> PatchGroupState -> Patch
                -> [(String, Bool)] -- snd indicates if only trailing part should
                                    -- be matched
                -> Bool
      evalQuery q pgState patch strings =
          eval q
          where
            eval (QPrim s) = any (matchesString s) strings
            eval (QAnd q1 q2) = eval q1 && eval q2
            eval (QOr q1 q2) = eval q1 || eval q2
            eval (QNot q') = not (eval q')
            eval (QState pState) = p_state patch == pState
            eval (QGroupState pgState') = pgState == pgState'
            eval (QPatchID id) = unPatchID (p_id patch) == id
            eval QReviewed = isReviewed patch
            eval QTrue = True
            eval QFalse = False
      matchesString queryStr (existingStr, trailingOnly) =
              let lowerQueryStr = map toLower queryStr
                  lowerExistingStr = map toLower existingStr
                  pred = if trailingOnly then List.isSuffixOf else List.isInfixOf
              in lowerQueryStr `pred` lowerExistingStr

allPatchIDs :: DPM [PatchID]
allPatchIDs =
    do patches <- readPatches
       return $ Map.keys patches

allConflicts :: DPM PatchConflicts
allConflicts =
    do model <- readModel
       return (M.allConflicts model)