packages feed

ampersand-3.0.3: src/Database/Design/Ampersand/FSpec/GenerateUML.hs

module Database.Design.Ampersand.FSpec.GenerateUML (generateUML) where

import Database.Design.Ampersand.Basics
import Database.Design.Ampersand.Core.AbstractSyntaxTree (explMarkup,aMarkup2String,Rule,Declaration,Purpose(..))
import Database.Design.Ampersand.FSpec.Graphic.ClassDiagram
import Database.Design.Ampersand.FSpec
import Data.Map (Map)
import Data.List
import qualified Data.Map as Map
import Control.Monad.State.Lazy  (State, gets, evalState, modify)

fatal :: Int -> String -> a
fatal = fatalMsg "FSpec.GenerateUML"

-- TODO: escape
-- TODO: names of model, package, assoc (empty?), etc.

generateUML :: FSpec -> String
generateUML fSpec = showUML (fSpec2UML fSpec)

showUML :: UML -> String
showUML uml = unlines $ evalState uml $ UMLState 0 Map.empty [] []

fSpec2UML :: FSpec -> UML
fSpec2UML fSpec =
 do { packageId0 <- mkUnlabeledId "TopPackage"
    ; packageId1 <- mkUnlabeledId "PackageClasses"
    ; packageId2 <- mkUnlabeledId "PackageReqs"
    ; diagramId <- mkUnlabeledId "Diagram"

    ; _ <- mapM (mkLabeledId "Datatype") datatypeNames
    ; _ <- mapM (mkLabeledId "Class") classNames

    ; datatypesUML <- mapM genUMLDatatype datatypeNames
    ; classesUML <- mapM genUMLClass (classes classDiag)
    ; assocsUML <- mapM genUMLAssociation (assocs classDiag)
    ; requirementsUML <- mapM genUMLRequirement (requirements fSpec)
    ; diagramElements <- genDiagramElements
    ; customProfileElements <- genCustomProfileElements
    ; customReqElements <- genCustomReqElements fSpec packageId2
    ; return $ [ "<?xml version=\"1.0\" encoding=\"UTF-8\"?>"
               , "<!-- Generated by "++ampersandVersionStr++" -->"
               , "<xmi:XMI xmi:version=\"2.1\" xmlns:uml=\"http://schema.omg.org/spec/UML/2.1\" xmlns:xmi=\"http://schema.omg.org/spec/XMI/2.1\" xmlns:thecustomprofile=\"http://www.sparxsystems.com/profiles/thecustomprofile/1.0\">"
               -- WHY is the exporter not something like `Ampersand` (in the string below)?
               -- BECAUSE then for some reason the importer doesn't show the properties of the requirements.
               , " <xmi:Documentation exporter=\"Enterprise Architect\" exporterVersion=\"6.5\"/>"
               , " <uml:Model xmi:type=\"uml:Model\" name=\""++contextName++"\" visibility=\"public\">"
               , "  <packagedElement xmi:type=\"uml:Package\" xmi:id="++show packageId0++" name="++show contextName++" visibility=\"public\">" ] ++
               [ "   <packagedElement xmi:type=\"uml:Package\" xmi:id="++show packageId1++" name="++show ("classesOf_"++contextName)++" visibility=\"public\">" ] ++
               concat datatypesUML ++
               concat classesUML ++
               concat assocsUML ++
               [ "   </packagedElement>" ] ++
               [ "   <packagedElement xmi:type=\"uml:Package\" xmi:id="++show packageId2++" name="++show ("RequirementsOf_"++contextName)++" visibility=\"public\">" ] ++
               concat requirementsUML ++
               [ "   </packagedElement>" ] ++
               [ "  </packagedElement>" ] ++
               customProfileElements ++
               [ " </uml:Model>"
               , " <xmi:Extension extender=\"Enterprise Architect\" extenderID=\"6.5\">"
               , "  <elements>"] ++
               [ "    <element xmi:idref="++show packageId0++" xmi:type=\"uml:Package\" name="++show contextName++" scope=\"public\">"]++
               [ "    </element>"]++
               [ "    <element xmi:idref="++show packageId1++" xmi:type=\"uml:Package\" name="++show ("classesOf_"++contextName)++" scope=\"public\">"]++
               [ "     <model package2="++show packageId1++" package="++show packageId0++" tpos=\"0\" ea_eleType=\"package\"/>"]++
               [ "    </element>"]++
               [ "    <element xmi:idref="++show packageId2++" xmi:type=\"uml:Package\" name="++show ("RequirementsOf_"++contextName)++" scope=\"public\">"]++
               [ "     <model package2="++show packageId2++" package="++show packageId0++" tpos=\"0\" ea_eleType=\"package\"/>"]++
               [ "    </element>"]++
               customReqElements ++
               [ "  </elements>"
               , "  <diagrams>"
               , "   <diagram xmi:id=\""++diagramId++"\">"
               , "    <model package=\""++packageId1++"\" owner=\""++packageId1++"\"/>"
               , "    <properties name=\"Data Model\" type=\"Logical\"/>"
               , "    <elements>" ] ++
               diagramElements ++
               [ "    </elements>"
               , "   </diagram>"
               , "  </diagrams>"
               , " </xmi:Extension>"
               , "</xmi:XMI>" ]
    }
 where classDiag = cdAnalysis fSpec
       contextName = cdName classDiag
       allConcs = ooCpts classDiag
       classNames    = map name (classes classDiag)
       datatypeNames = map name allConcs >- classNames

genUMLRequirement :: Req -> UML
genUMLRequirement req =
 do { reqLId <- mkUnlabeledId "Req"
    ; addReqToState (reqLId, req)
    ; return $ [ "    <packagedElement xmi:type=\"uml:Class\" xmi:id=\""++reqLId++"\" name=\""++reqId req++"\" visibility=\"public\"/> " ]
    }

genUMLDatatype :: String -> UML
genUMLDatatype nm =
 do { datatypeId <- refLabeledId nm
    ; addToDiagram datatypeId
    ; return [ "    <packagedElement xmi:type=\"uml:DataType\" xmi:id=\""++datatypeId++"\" name=\""++nm++"\" visibility=\"public\"/> " ]
    }

genUMLClass :: Class -> UML
genUMLClass cl =
 do { classId <- refLabeledId (clName cl)
    ; addToDiagram classId
    ; attributesUML <- mapM genUMAttribute (clAtts cl)
    ; return $ [ "    <packagedElement xmi:type=\"uml:Class\" xmi:id=\""++classId++"\" name=\""++clName cl++"\" visibility=\"public\">"] ++
               concat attributesUML ++
               [ "    </packagedElement>"]
    }

genUMAttribute :: CdAttribute -> UML
genUMAttribute  (OOAttr nm attrType optional) =
 do { attrId <- mkUnlabeledId "Attr"
    ; lIntId <- mkUnlabeledId "Int"
    ; uIntId <- mkUnlabeledId "Int"
    ; classId <- refLabeledId attrType
    ; return [ "       <ownedAttribute xmi:type=\"uml:Property\" xmi:id=\""++attrId++"\" name=\""++nm++"\" visibility=\"public\" isStatic=\"false\""++
                                    " isReadOnly=\"false\" isDerived=\"false\" isOrdered=\"false\" isUnique=\"true\" isDerivedUnion=\"false\">"
             , "        <type xmi:idref=\""++classId++"\"/>"
             , "        <lowerValue xmi:type=\"uml:LiteralInteger\" xmi:id=\""++lIntId++"\" value=\""++(if optional then "0" else "1")++"\"/>"
             , "        <upperValue xmi:type=\"uml:LiteralInteger\" xmi:id=\""++uIntId++"\" value=\"1\"/>"
             , "       </ownedAttribute>"]
    }

genUMLAssociation :: Association -> UML
genUMLAssociation ass =
 do { assocId <- mkUnlabeledId "Assoc"
    ; lMemberAndOwnedEnd <- genMemberAndOwnedEnd (asslhm ass) assocId (assSrc ass)
    ; rMemberAndOwnedEnd <- genMemberAndOwnedEnd (assrhm ass) assocId (assTgt ass)

    ; return $
        [ "    <packagedElement xmi:type=\"uml:Association\" xmi:id=\""++assocId++"\" name=\""++assrhr ass++"\" visibility=\"public\">"
        ] ++
        lMemberAndOwnedEnd ++
        rMemberAndOwnedEnd ++
        [ "    </packagedElement>"
        ]
    }
 where genMemberAndOwnedEnd (Mult minVal maxVal) assocId type' =
        do { endId <- mkUnlabeledId "MemberEnd"
           ; typeId <- refLabeledId type'
           ; lIntId <- mkUnlabeledId "Int"
           ; uIntId <- mkUnlabeledId "Int"
           ; return
               [ "     <memberEnd xmi:idref=\""++endId++"\"/>"
               , "     <ownedEnd xmi:type=\"uml:Property\" xmi:id=\""++endId++"\" visibility=\"public\" association=\""++assocId++"\" isStatic=\"false\""++
                              " isReadOnly=\"false\" isDerived=\"false\" isOrdered=\"false\" isUnique=\"true\" isDerivedUnion=\"false\" aggregation=\"none\">"
               , "      <type xmi:idref=\""++typeId++"\"/>"
               , "      <lowerValue xmi:type=\"uml:LiteralInteger\" xmi:id=\""++lIntId++"\" value=\""++(if minVal == MinZero then "0" else "1")++"\"/>"
               , case maxVal of
                   MaxOne  -> "      <upperValue xmi:type=\"uml:LiteralInteger\" xmi:id=\""++uIntId++"\" value=\"1\"/>"
                   MaxMany -> "      <upperValue xmi:type=\"uml:LiteralUnlimitedNatural\" xmi:id=\""++uIntId++"\" value=\"-1\"/>"
               , "     </ownedEnd>"
               ]
           }

genDiagramElements :: UML
genDiagramElements =
 do { elementIds <- gets diagramEltIds
    ; return [ "      <element subject=\""++elementId++"\"/>" | elementId <- elementIds ]
    }

genCustomProfileElements :: UML
genCustomProfileElements =
 do { reqVals <- gets reqValues
    ; return  [reqUML req | req <- reverse reqVals]
    }
  where
    reqUML :: ReqValue2 -> String
    reqUML (xmiId, req) = intercalate "\n"
     ( ["   <thecustomprofile:Functional base_Requirement="++show xmiId++"/>"]++
       [tagUML xmiId count puprtxt reftxt | (count, (puprtxt, reftxt)) <- zip [0::Int ..] [(aMarkup2String (explMarkup p), intercalate ";" (explRefIds p)) | p <- reqPurposes req]]
     )
    tagUML xmiId nr value reftxt = intercalate "\n"
      [ "     <thecustomprofile:"++keyMeaning++" base_Requirement="++show xmiId++" "++keyMeaning++"="++show value++"/>"
      , "     <thecustomprofile:"++keyRef    ++" base_Requirement="++show xmiId++" "++keyRef++"="++show reftxt++"/>"
      ]
        where keyMeaning = "Meaning"++show nr
              keyRef     = "Reference"++show nr

genCustomReqElements :: FSpec -> String -> UML
genCustomReqElements fSpec parentPackageId =
 do { reqVals <- gets reqValues
    ; return [reqUML req | req <- reverse reqVals]
    }
  where
    reqUML :: ReqValue2 -> String
    reqUML (xmiId, req) = intercalate "\n"
     ([ "    <element xmi:idref="++show xmiId++" xmi:type=\"uml:Requirement\" name="++show (reqId req)++" scope=\"public\""++">"
      , "      <model package="++show parentPackageId++" ea_eleType=\"element\"/>"
      , "      <properties documentation="++show (maybe "" aMarkup2String (meaning (fsLang fSpec) req))++" isSpecification=\"false\" sType=\"Requirement\" nType=\"0\" scope=\"public\" stereotype=\"Functional\"/>"
      , "      <tags>"]++
      [ "         <tag name=\"Purpose"++nr++"\" value="++show p++" modelElement="++show xmiId++"/>" | (nr ,p) <- zip ("" : map show [1::Int ..]) ([aMarkup2String (explMarkup p) | p <- reqPurposes req])  ]++
      [ "      </tags>"
      , "    </element>"
      ])

-- Requirements

data Req = Req { reqId :: String
            --   , reqRef :: String
               , reqOrig :: Either Rule Declaration
               , reqPurposes :: [Purpose]
               }

instance Meaning Req where
  meaning l r = case reqOrig r of
                  Right rul -> meaning l rul
                  Left  dcl -> meaning l dcl

requirements :: FSpec -> [Req]
requirements fSpec
   = [decl2req d | d <- vrels  fSpec]
   ++[rule2req r | r <- vrules fSpec]
  where
    decl2req d = Req { reqId = name d
                     , reqOrig = Right d
                     , reqPurposes = purposesDefinedIn fSpec (fsLang fSpec) d
                     }
    rule2req r = Req { reqId = name r
                     , reqOrig = Left r
                     , reqPurposes = purposesDefinedIn fSpec (fsLang fSpec) r
                     }

-- State and Monad

data UMLState = UMLState { idCounter :: Int
                         , labelIdMap :: Map String String
                         , diagramEltIds :: [String]
                         , reqValues :: [ReqValue2]
                         }

type StateUML a = State UMLState a

type UML = StateUML [String]

type ReqValue2 = ( String -- the xmi-id
                , Req
                )

addToDiagram :: String -> StateUML ()
addToDiagram elementId =
  modify $ \state' -> state' { diagramEltIds = elementId : diagramEltIds state'}

addReqToState :: ReqValue2 -> StateUML ()
addReqToState reqVal =
  modify $ \state' -> state' { reqValues = reqVal : reqValues state'}

mkUnlabeledId :: String -> StateUML String
mkUnlabeledId tag =
 do { idC <- gets idCounter
    ; modify $ \state' -> state' { idCounter = idCounter state' + 1}
    ; let unlabeledId = tag++"ID_"++show idC
    ; return unlabeledId
    }

refLabeledId :: String -> StateUML String
refLabeledId label =
 do { lidMap <- gets labelIdMap
    ; case Map.lookup label lidMap of
          Just lid -> return lid
          Nothing  -> fatal 147 $ "Requesting non-existent label "++label
    }

mkLabeledId :: String -> String -> StateUML ()
mkLabeledId tag label =
 do { let classId = tag++"ID_"++label
    ; modify $ \state' -> state' { labelIdMap =  Map.insert label classId (labelIdMap state') }
    }