llsd-0.1.0.1: Network/Format/LLSD/XML.hs
-- |
-- Module : Network.Format.LLSD.XML
-- Copyright : (c) Linden Lab 2009
-- License : MIT
-- Maintainer : bos@lindenlab.com
-- Stability : provisional
-- Portability : portable
module Network.Format.LLSD.XML (
parseXML,
formatXML,
)
where
import Codec.Binary.Base64 as Base64
import Control.Monad.Error
import Data.Char (isSpace)
import qualified Data.ByteString as B
import qualified Data.ByteString.Lazy as L
import Data.List (foldl')
import qualified Data.Map as Map
-- import qualified Data.Text as T
import Data.Time (UTCTime)
import Data.UUID (UUID)
import Network.Format.LLSD.Internal
import Network.URI (URI)
import qualified Text.XML.Expat.Tree as XML
import qualified Text.XML.Expat.Format as XML
type XText = B.ByteString
toXText :: String -> XText
toXText = XML.gxFromString
fromXText :: XText -> String
fromXText = XML.gxToString
type Node = XML.Node String XText
parseXML :: B.ByteString -> Either String LLSD
parseXML s = do
case XML.parseTree' Nothing s of
Right (XML.Element "llsd" _ cs) -> convertLLSD cs
Right (_) -> fail "root element not llsd"
Left err -> (fail . show) err
convertLLSD :: [Node] -> Either String LLSD
convertLLSD ns
| exactlyOne filtns = convertValue (head filtns)
| otherwise = fail "not just one element under llsd"
where filtns = dropWhitespaceText ns
exactlyOne [_] = True
exactlyOne _ = False
convertValue :: Node -> Either String LLSD
convertValue (XML.Element name _ ns) =
case name of
"undef" -> convertUndef ns
"boolean" -> textOnly ns >>= convertBool
"integer" -> textOnly ns >>= convertInt
"real" -> textOnly ns >>= convertReal
"string" -> textOnly ns >>= convertString
"uuid" -> textOnly ns >>= convertUUID
"date" -> textOnly ns >>= convertDate
"uri" -> textOnly ns >>= convertURI
"binary" -> textOnly ns >>= convertBinary
"map" -> convertMap ns
"array" -> convertArray ns
_ -> fail ("unknown value element: " ++ name)
convertValue (XML.Text text) = fail ("Unexpected text: " ++ fromXText text)
convertUndef :: [Node] -> Either String LLSD
convertUndef ns = case dropWhitespaceText ns of
[] -> return undef
_ -> fail "undef element isn't empty"
convertBool, convertInt, convertReal, convertString, convertUUID,
convertDate, convertURI, convertBinary :: String -> Either String LLSD
convertBool t = fmap toLLSD $ textBoolean t
convertInt t = return $ toLLSD (fromLLSD (toLLSD t) :: Int)
convertReal t = return $ toLLSD (fromLLSD (toLLSD t) :: Double)
convertString t = return $ toLLSD t
convertUUID t = return $ toLLSD (fromLLSD (toLLSD t) :: UUID)
convertDate t = return $ toLLSD (fromLLSD (toLLSD t) :: UTCTime)
convertURI t = return $ toLLSD (fromLLSD (toLLSD t) :: URI)
convertBinary t =
maybe (fail "invalid base64") (return . toLLSD . L.pack) $ Base64.decode t
-- FIXME: should check that the encoding attribute is "base64"
convertArray :: [Node] -> Either String LLSD
convertArray = fmap toLLSD . mapM convertValue . dropWhitespaceText
convertMap :: [Node] -> Either String LLSD
convertMap ns = (toLLSD . Map.fromAscList) `fmap`
(mapM convertAssoc =<< pairUp (dropWhitespaceText ns))
where pairUp (a:b:rest) = do { ps <- pairUp rest; return $ (a,b) : ps }
pairUp (_:[]) = fail "extra element in map"
pairUp _ = return []
convertAssoc :: (Node, Node) -> Either String (String, LLSD)
convertAssoc (nk, nv) = (,) `fmap` convertKey nk `ap` convertValue nv
convertKey :: Node -> Either String String
convertKey (XML.Element "key" _ ns) = textOnly ns
convertKey (XML.Element name _ _) = fail ("expected key, found element: " ++ name)
convertKey (XML.Text text) = fail ("expected key, found text: " ++ fromXText text)
dropWhitespaceText :: [Node] -> [Node]
dropWhitespaceText ns = filter isNotWhitespace ns
where isNotWhitespace (XML.Element _ _ _) = True
isNotWhitespace (XML.Text t) = not $ all isSpace $ fromXText t
textOnly :: [Node] -> Either String String
textOnly ns = (foldl' (\a b -> a ++ fromXText b) "") `fmap` textOnly' ns
where textOnly' :: [Node] -> Either String [B.ByteString]
textOnly' [] = return []
textOnly' ((XML.Text t):ns') = textOnly' ns' >>= return . (t:)
textOnly' ((XML.Element name _ _):_) =
fail ("expected only text, found element: " ++ name)
formatXML :: LLSD -> L.ByteString
formatXML v = XML.formatNode $ XML.Element "llsd" [] [asNode v]
-- FIXME: ? using XML.formatTree outputs an XML header
asNode :: LLSD -> Node
asNode LUndef = XML.Element "undef" [] []
asNode (LBool b) = XML.Element "boolean" [] [XML.Text $ booleanText b]
asNode v@(LInt _) = XML.Element "integer" [] (textOf v)
asNode v@(LReal _) = XML.Element "real" [] (textOf v)
asNode v@(LString _) = XML.Element "string" [] (textOf v)
asNode v@(LUUID _) = XML.Element "uuid" [] (textOf v)
asNode v@(LDate _) = XML.Element "date" [] (textOf v)
asNode v@(LURI _) = XML.Element "uri" [] (textOf v)
asNode (LBinary b) = XML.Element "binary" [("encoding", toXText "base64")] $
base64Text b
asNode (LArray vs) = XML.Element "array" [] $
map asNode $ expandLLSDArray vs
asNode (LMap vs) = XML.Element "map" [] $
concatMap asAssocNodes $ Map.assocs vs
where asAssocNodes (k, v) = [XML.Element "key" [] [XML.Text $ toXText k], asNode v]
booleanText :: Bool -> B.ByteString
booleanText True = toXText "true"
booleanText False = toXText "false"
textBoolean :: String -> Either String Bool
textBoolean "true" = return True
textBoolean "false" = return False
textBoolean t = fail $ "invalid value for boolean: " ++ t
-- FIXME: The spec implies that the value for the boolean node should be
-- the string conversion, but that would make False be the empty string.
-- The C++ implementation generates the word "false". Who's right?
textOf :: LLSD -> [Node]
textOf v = case t of
"" -> [];
_ -> [XML.Text $ toXText t]
where t = fromLLSD v :: String
base64Text :: L.ByteString -> [Node]
base64Text b = if L.null b
then []
else [XML.Text $ toXText $ Base64.encode $ L.unpack b]