]> gitweb @ CieloNegro.org - Rakka.git/blobdiff - Rakka/SystemConfig.hs
merge branch origin/master
[Rakka.git] / Rakka / SystemConfig.hs
index 58de2bec0ca22afb68ce1534dc23691617136cd1..d15bc9d99a0f2bc6edb65ed467b62631d9e6a964 100644 (file)
@@ -1,3 +1,9 @@
+{-# LANGUAGE
+    DeriveDataTypeable
+  , OverloadedStrings
+  , ScopedTypeVariables
+  , UnicodeSyntax
+  #-}
 module Rakka.SystemConfig
     ( SystemConfig
     , SysConfValue(..)
@@ -6,153 +12,278 @@ module Rakka.SystemConfig
 
     , getSysConf
     , getSysConfA
+
+    , setSysConf
+    , setSysConfA
+
+    , SiteName(..)
+    , BaseURI(..)
+    , DefaultPage(..)
+    , StyleSheet(..)
+    , Languages(..)
+    , GlobalLock(..)
+
+    , serializeTextPairs
+    , deserializeTextPairs
+    , serializeMap
+    , deserializeMap
     )
     where
-
+import Control.Applicative
+import           Codec.Binary.UTF8.String
 import           Control.Arrow.ArrowIO
+import Control.Arrow.Unicode
 import           Control.Concurrent.STM
+import           Control.Monad
 import           Control.Monad.Trans
+import Control.Monad.Unicode
 import qualified Data.ByteString.Char8 as C8
-import           Data.Encoding
-import           Data.Encoding.UTF8
+import qualified Data.ByteString.Lazy  as L
+import qualified Data.CaseInsensitive as CI
+import           Data.Dynamic
 import           Data.Map (Map)
 import qualified Data.Map as M
 import           Data.Maybe
+import Data.Monoid.Unicode
+import Data.Text (Text)
+import qualified Data.Text as T
+import qualified Data.Text.Encoding as T
 import           GHC.Conc (unsafeIOToSTM)
-import           Network
+import           Network.BSD
 import qualified Network.HTTP.Lucu.Config as LC
-import           Network.URI
+import           Network.HTTP.Lucu hiding (Config)
+import           Network.URI hiding (path)
+import Prelude.Unicode
+import           Rakka.Page
 import           Rakka.Utils
 import           Subversion.FileSystem
 import           Subversion.FileSystem.Revision
 import           Subversion.FileSystem.Root
+import           Subversion.FileSystem.Transaction
 import           Subversion.Repository
+import           Subversion.Types
 import           System.FilePath.Posix
+import System.IO.Unsafe
 import           System.Log.Logger
 
+logger :: String
 logger = "Rakka.SystemConfig"
 
 
 data SystemConfig = SystemConfig {
       scLucuConf   :: !LC.Config
     , scRepository :: !Repository
-    , scCache      :: !(TVar (Map FilePath SysConfValue))
+    , scCache      :: !(TVar (Map FilePath Dynamic))
     }
 
-
-data SysConfValue
-    = SiteName String
-    | BaseURI URI
-    | DefaultPage String
-    | StyleSheet String
-    deriving (Eq, Show)
-
+class (Typeable α, Show α, Eq α) ⇒ SysConfValue α where
+    confPath     ∷ α → FilePath
+    serialize    ∷ α → Text
+    deserialize  ∷ Text → Maybe α
+    defaultValue ∷ SystemConfig → α
 
 mkSystemConfig :: LC.Config -> Repository -> IO SystemConfig
 mkSystemConfig lc repos
     = do cache <- newTVarIO M.empty
-         return SystemConfig {
+         return SystemConfig {
                       scLucuConf   = lc
                     , scRepository = repos
                     , scCache      = cache
                     }
 
-
-getSysConf :: MonadIO m => SystemConfig -> SysConfValue -> m SysConfValue
-getSysConf sc key
-    = liftIO $
-      atomically $
-      do let path = sysConfPath key
-
-         cache <- readTVar (scCache sc)
-
+getSysConf ∷ ∀m a. (MonadIO m, SysConfValue a) ⇒ SystemConfig → m a
+getSysConf sc
+    = liftIO $ atomically $
+      do cache ← readTVar (scCache sc)
+         let path = confPath ((⊥) ∷ a)
          case M.lookup path cache of
-           Just val -> return val
-           Nothing  -> do val <- unsafeIOToSTM (getSysConf' sc key)
-                          writeTVar (scCache sc) (M.insert path val cache)
-                          return val
-
-
-getSysConf' :: SystemConfig -> SysConfValue -> IO SysConfValue
-getSysConf' sc key
-    = do let path = fromConfPath (sysConfPath key)
-
-         fs    <- getRepositoryFS (scRepository sc)
-         rev   <- getYoungestRev fs
-         value <- withRevision fs rev
-                  $ do exists <- isFile path
-                       case exists of
-                         True
-                             -> do str <- getFileContentsLBS path
-                                   return $ Just $ chomp $ decodeLazy UTF8 str
-                         False
-                             -> return Nothing
-
+           Just val → pure ∘ fromJust $ fromDynamic val
+           Nothing  → do val ← unsafeIOToSTM (getSysConf' sc)
+                         writeTVar (scCache sc) (M.insert path (toDyn val) cache)
+                         return val
+
+getSysConf' ∷ ∀α. SysConfValue α ⇒ SystemConfig → IO α
+getSysConf' sc
+    = do let path = fromConfPath $ confPath ((⊥) ∷ α)
+         fs    ← getRepositoryFS (scRepository sc)
+         rev   ← getYoungestRev fs
+         value ← withRevision fs rev
+                 $ do exists ← isFile path
+                      case exists of
+                        True
+                            → do str ← getFileContentsLBS path
+                                 return $ Just $ T.pack $ chomp $ decode $ L.unpack str
+                        False
+                            → return Nothing
          case value of
            Just str
-               -> do let val = unmarshalSysConf key str
-                     debugM logger ("Got a config value at `" ++ path ++ "': " ++ show val)
-                     return val
+               → case deserialize str of
+                   Just val
+                       → debugM logger ("Got a config value at `" ⊕ path ⊕ "': " ⊕ show val) ≫
+                         return val
+                   Nothing
+                       → fail ("Got an invalid config value at `" ⊕ path ⊕ "': " ⊕ show str)
            Nothing
-               -> do val <- sysConfDefault sc key
-                     debugM logger ("Got no config value at `" ++ path ++ "'. Defaulting to " ++ show val)
-                     return val
-
-
-getSysConfA :: ArrowIO a => SystemConfig -> SysConfValue -> a b SysConfValue
-getSysConfA = (arrIO0 .) . getSysConf
-
-
-fromConfPath :: FilePath -> FilePath
-fromConfPath = combine "/config"
+               → do let val = defaultValue sc
+                    debugM logger ("Got no config value at `" ⊕ path ⊕ "'. Defaulting to " ⊕ show val)
+                    return val
 
+setSysConf :: forall m a. (MonadIO m, SysConfValue a) => SystemConfig -> String -> a -> m StatusCode
+setSysConf sc userID value
+    = liftIO $
+      do let path = confPath (undefined :: a)
+
+         current <- getSysConf sc
+        if current == value
+           then return NotModified
+           else do atomically $ do cache <- readTVar (scCache sc)
+                                    writeTVar (scCache sc) (M.delete path cache)
+                   setSysConf' sc userID value
+
+
+setSysConf' ∷ ∀α. SysConfValue α ⇒ SystemConfig → String → α → IO StatusCode
+setSysConf' sc userID value
+    = do let path  = fromConfPath $ confPath ((⊥) ∷ α)
+            str   = (L.fromChunks ∘ (:[]) ∘ T.encodeUtf8 $ serialize value) ⊕ "\n"
+            repos = scRepository sc
+         fs  ← getRepositoryFS repos
+        rev ← getYoungestRev fs
+         ret ← doReposTxn
+               repos
+               rev
+               userID
+               (Just "Automatic commit by Rakka for systemConfig update")
+               $ do exists ← isFile path
+                    unless exists
+                        $ createValueEntry path
+                    applyTextLBS path Nothing str
+        case ret of
+          Left  _ → return Conflict
+          Right _ → do debugM logger ("Set a config value at `" ⊕ path ⊕ "': " ⊕ show value)
+                        return Created
+    where
+    createValueEntry ∷ FilePath → Txn ()
+    createValueEntry path
+        = do createParentDirectories path
+             makeFile path
 
-{- paths -}
-sysConfPath :: SysConfValue -> FilePath
-sysConfPath (SiteName    _) = "siteName"
-sysConfPath (BaseURI     _) = "baseURI"
-sysConfPath (DefaultPage _) = "defaultPage"
-sysConfPath (StyleSheet  _) = "styleSheet"
+    createParentDirectories ∷ FilePath → Txn ()
+    createParentDirectories path
+        = do let parentPath = takeDirectory path
+             kind ← checkPath parentPath
+             case kind of
+               NoNode   → createParentDirectories parentPath ≫
+                          makeDirectory parentPath
+               FileNode → fail ("createParentDirectories: already exists a file: " ⊕ parentPath)
+               DirNode  → return ()
 
+getSysConfA :: (ArrowIO a, SysConfValue c) => SystemConfig -> a b c
+getSysConfA = arrIO0 . getSysConf
 
-{- marshalling -}
-marshalSysConf :: SysConfValue -> String
-marshalSysConf (SiteName    name) = name
-marshalSysConf (BaseURI     uri ) = uriToString id uri ""
-marshalSysConf (DefaultPage name) = name
-marshalSysConf (StyleSheet  name) = name
 
+setSysConfA :: (ArrowIO a, SysConfValue c) => SystemConfig -> String -> a c StatusCode
+setSysConfA = (arrIO .) . setSysConf
 
-{- unmarshalling -}
-unmarshalSysConf :: SysConfValue -> String -> SysConfValue
-unmarshalSysConf (SiteName    _) name = SiteName name
-unmarshalSysConf (BaseURI     _) uri  = BaseURI $ fromJust $ parseURI uri
-unmarshalSysConf (DefaultPage _) name = DefaultPage name
-unmarshalSysConf (StyleSheet  _) name = StyleSheet name
 
+fromConfPath :: FilePath -> FilePath
+fromConfPath = ("/config" </>)
 
-{- getting default value -}
-sysConfDefault :: SystemConfig -> SysConfValue -> IO SysConfValue
+serializeTextPairs ∷ [(Text, Text)] → Text
+serializeTextPairs = T.intercalate "\n" ∘ (serializePair' <$>)
+    where
+      serializePair' ∷ (Text, Text) → Text
+      serializePair' (a, b) = a ⊕ " " ⊕ b
 
-sysConfDefault _ (SiteName _)
-    = return $ SiteName "Rakka"
+serializeMap ∷ (k → Text) → (v → Text) → Map k v → Text
+serializeMap f g = serializeTextPairs ∘ ((f ⁂ g) <$>) ∘ M.toList
 
-sysConfDefault sc (BaseURI _)
-    = do let conf = scLucuConf sc
-             host = C8.unpack $ LC.cnfServerHost conf
-             port = case LC.cnfServerPort conf of
-                      PortNumber num -> fromIntegral num
-             
-             defaultURI
+deserializeTextPairs ∷ Text → Maybe [(Text, Text)]
+deserializeTextPairs = mapM deserializePair' ∘ T.lines
+    where
+      deserializePair' ∷ Text → Maybe (Text, Text)
+      deserializePair' s = case T.breakOn " " s of
+                             (a, b)
+                                 | (¬) (T.null b) → Just (a, T.tail b)
+                             _                    → Nothing
+
+deserializeMap ∷ Ord k ⇒ (Text → k) → (Text → v) → Text → Maybe (Map k v)
+deserializeMap f g = (M.fromList ∘ ((f ⁂ g) <$>) <$>) ∘ deserializeTextPairs
+
+newtype SiteName = SiteName Text deriving (Show, Typeable, Eq)
+instance SysConfValue SiteName where
+    confPath _                = "siteName"
+    serialize (SiteName name) = name
+    deserialize               = Just . SiteName
+    defaultValue _            = SiteName "Rakka"
+
+newtype BaseURI = BaseURI URI deriving (Show, Typeable, Eq)
+instance SysConfValue BaseURI where
+    confPath _              = "baseURI"
+    serialize (BaseURI uri) = T.pack $ uriToString id uri ""
+    deserialize uri         = fmap BaseURI
+                              $ do parsed ← parseURI (T.unpack uri)
+                                   when (uriPath parsed        ≡ "" ) mzero
+                                   when (last (uriPath parsed) ≠ '/') mzero
+                                   when (uriQuery parsed       ≠ "" ) mzero
+                                   when (uriFragment parsed    ≠ "" ) mzero
+                                   return parsed
+    defaultValue sc
+        = let conf = scLucuConf sc
+              host = C8.unpack $ LC.cnfServerHost conf
+              port = unsafePerformIO $
+                     do ent <- getServiceByName (LC.cnfServerPort conf) "tcp"
+                        return (servicePort ent)
+              -- FIXME: There should be a way to change configurations
+              -- without web interface nor direct repository
+              -- modification.
+              defaultURI
                   = "http://" ++ host ++
                     (if port == 80
                      then ""
                      else ':' : show port) ++ "/"
-
-         return $ BaseURI $ fromJust $ parseURI defaultURI
-
-sysConfDefault _ (DefaultPage _)
-    = return $ DefaultPage "MainPage"
-
-sysConfDefault _ (StyleSheet _)
-    = return $ StyleSheet "StyleSheet/Default"
+          in
+            BaseURI $ fromJust $ parseURI defaultURI
+
+newtype DefaultPage = DefaultPage Text deriving (Show, Typeable, Eq)
+instance SysConfValue DefaultPage where
+    confPath _                   = "defaultPage"
+    serialize (DefaultPage name) = name
+    deserialize                  = Just . DefaultPage
+    defaultValue _               = DefaultPage "MainPage"
+
+newtype StyleSheet = StyleSheet Text deriving (Show, Typeable, Eq)
+instance SysConfValue StyleSheet where
+    confPath _                  = "styleSheet"
+    serialize (StyleSheet name) = name
+    deserialize                 = Just . StyleSheet
+    defaultValue _              = StyleSheet "StyleSheet/Default"
+
+newtype Languages = Languages (Map LanguageTag LanguageName) deriving (Show, Typeable, Eq)
+instance SysConfValue Languages where
+    confPath _                  = "languages"
+    serialize (Languages langs) = serializeMap CI.foldedCase id langs
+    deserialize                 = (Languages <$>) ∘ deserializeMap CI.mk id
+    defaultValue _         
+        = Languages $ M.fromList [ ("en", "English"  )
+                                 , ("es", "Español"  )
+                                 , ("de", "Deutsch"  )
+                                 , ("fi", "Suomi"    )
+                                 , ("fr", "Français" )
+                                 , ("ga", "Gaeilge"  )
+                                 , ("gd", "Gàidhlig" )
+                                 , ("ja", "日本語"  )
+                                 , ("pt", "Português")
+                                 , ("sv", "Svenska"  )
+                                 ]
+
+
+newtype GlobalLock = GlobalLock Bool deriving (Show, Typeable, Eq)
+instance SysConfValue GlobalLock where
+    confPath _      = "globalLock"
+    serialize (GlobalLock isLocked)
+        | isLocked  = "*"
+        | otherwise = ""
+    deserialize "*" = Just (GlobalLock True)
+    deserialize ""  = Just (GlobalLock False)
+    deserialize _   = Nothing
+    defaultValue _  = GlobalLock False