packages feed

j2hs-0.99: src/Java2Haskell.hss

{-# LANGUAGE Haskell2010
 #-}
{-# OPTIONS
    -Wall
    -fno-warn-name-shadowing
 #-}

{- | The @j2hs@ command - create Haskell bindings for Java classes.
    
-}
module Java2Haskell where

import Prelude hiding (print)

import Options
import Utils
import Types
import Segments
import CodeGen.Class
import CodeGen.JavaBindings

import Foreign.Java
import Foreign.Java.IO

import Language.Java.Reflect

import Data.NamedRecord
import qualified Data.List as L
import Data.Strings

import Data.Generics

import Data.Map (Map)
import MultiMap (MultiMap)
import qualified Data.Map as Map
import qualified Data.Set as Set
import qualified MultiMap as MultiMap

import Control.Applicative
import Control.Monad
import Control.Monad.IO.Class

import System.Directory
import System.FilePath
import System.IO hiding (print)
import qualified System.IO.Strict as Strict
import System.Exit

import Haskell.X


ifVerbose
    :: MonadIO m
    => Options
    -> String
    -> m ()
ifVerbose opts = when (opts `get` optVerbose) . liftIO . putStr

-- | Invoked by Main.main, the core functionality
j2hs
    :: Options
    -> [String]
    -> IO ()
j2hs opts args = do

    let augmentEnvironment = do
            targetDir <- return (opts `get` optTargetDirectory)
            createDirectoryIfMissing True targetDir

            classpath <- mapM canonicalizePath $ opts `get` optClasspath

            cabalExtra <- if null (opts `get` optCabalPreset)
                then return ""
                else Strict.readFile (opts `get` optCabalPreset)

            setCurrentDirectory targetDir

            return $ opts
              `set` optClasspath := classpath
              `set` optTargetDirectory := targetDir
              `set` optCabalExtra := cabalExtra

    reflectDump <- if null (opts `get` optLoadReflectDump)
        then return Nothing
        else Just <$> canonicalizePath (opts `get` optLoadReflectDump)

    opts <- augmentEnvironment

    let classpath = opts `get` optClasspath
        classpath' = concat $ L.intersperse ":" classpath

    ifVerbose opts """
        Classpath:
        #{concatMap (\x -> "  " ++ x ++ "\n") (classpath)}"""
    print "Initializing JVM..."
    initJava ["""-Djava.class.path=#{classpath'}"""]
    println " Done."
    
    unless (null args) $ runJava $ do
        io $ do
            ifVerbose opts """
                Target Directory:
                  #{opts `get` optTargetDirectory}
                """
            
        ifVerbose opts """
            Looking for Java classes:
            #{concatMap (\x -> "  " ++ x ++ "\n") args}"""

        classInfo <- gatherClassInfo opts reflectDump args

        when (opts `get` optOnlyReflect) $ io $ do
            mapM_ println $ Map.elems classInfo
            exitSuccess

        -- Calculate the Haskell module names for packages and classes
        let classes = Map.keys classInfo

            packages = Set.toList $ Set.fromList
                     $ map (maybe "" id . takePackageName) classes

            -- temporary class names
            classModules_ = foldr (uncurry g) xEmpty $ map f classes
              where f x = (x, makeClassModuleName x)
                    g clazzName modName all = xInsert clazzName newName all
                      where newName = head $ dropWhile (`xMemberR` all)
                                           $ iterate (++ "_") modName

            -- package names.
            -- This func keeps in mind that package names must not clash with class names.
            packageModules = foldr (uncurry g) xEmpty $ map f packages
              where f x = (x, makePackageModuleName x)
                    g pkgName modName all = xInsert pkgName newName all
                      where newName = head $ dropWhile (\n -> n `xMemberR` classModules_
                                                           || n `xMemberR` all)
                                           $ iterate (++ "Package") modName

            bimapCheckedLookup bimap name
                = maybe (error """Failed lookup in Bimap: #{show name}.""") id $ xLookup bimap name

            mapCheckedLookup map name
                = maybe (error """Failed lookup in Map: #{show name}""") id $ name `Map.lookup` map

            -- finally the class names.
            -- This func keeps in mind that the package names might have been augmented
            -- before. i.e. the package java.awt.image will be Java.Awt.ImagePackage now,
            -- thus all classes inside the package have their full name changed too.
            classModules :: Map String String
            classModules = Map.fromList $ map f $ xToList classModules_
              where f (clazzName, moduleName)
                        = (clazzName, joinClassName (newPackageName, classModuleName))
                      where classModuleName = takeClassName moduleName
                            packageName = maybe "" id $ takePackageName clazzName
                            newPackageName = bimapCheckedLookup packageModules packageName

            -- A mapping from packages to classes
            classesByPackage :: MultiMap String String
            classesByPackage = MultiMap.fromList
                             $ map (\clazz -> (fst $ splitClassName clazz, clazz)) classes

        -- Find a suitable segmentation
        rankedClusters <- findClassClusters classInfo

        let segmentSize = min (fromIntegral (opts `get` optSegmentSize)) (Map.size classInfo)
            segments = segment3 segmentSize rankedClusters
            lengths = concat $ L.intersperse ", " $ map (show . length) segments

        println """Identified #{show $ length segments} segments of lengths (#{lengths})."""

        -- Assemble the class info
        let info = ClassInfo {
          aboutClass = mapCheckedLookup classInfo,
          classesForPackage = flip MultiMap.lookup classesByPackage,
          classModName = mapCheckedLookup classModules,
          packageModName = bimapCheckedLookup packageModules,
          allClasses = Map.keys classModules,
          allPackages = xKeys packageModules
        }

        -- Create the code files.
        let pName = opts `get` optProjectName
            pVersion = opts `get` optProjectVersion
            mkDeps = map (\i -> pName ++ "-part" ++ show i ++ " == " ++ pVersion)

        cwd <- io $ getCurrentDirectory
        forM_ (zip [1 :: Int ..] segments) $ \(i, segment) -> do
            io $ do
                setCurrentDirectory cwd
                createDirectoryIfMissing True (show i)
                setCurrentDirectory (show i)
            let opts' = opts
                  `upd` optProjectName  := (++ "-part" ++ show i)
                  `upd` optDependencies := (++ mkDeps [1..pred i])
            generateClassModules opts' info segment
        io $ setCurrentDirectory cwd

        let deps = mkDeps [1..length segments]
        generatePackageModules (opts `upd` optDependencies := (++ deps)) info (allPackages info)
    return ()

-- | Gather information about classes via Reflection
-- or (depending on the options set) from a dump file.
gatherClassInfo
    :: Options
    -> Maybe FilePath
    -> [String]
    -> Java (Map String JavaClass)
gatherClassInfo opts reflectDump classNames = do

    reflectClasses <- getReflectClasses

    print """Gathering reflection information..."""
    let getClassInfo :: Java (Map String JavaClass)
        getClassInfo = everywhere (mkT (\(TyVar v) -> TyVar ('_':v)))
            `fmap` reflectClasses True classNames

        readClassInfo :: String -> Java (Map String JavaClass)
        readClassInfo file = io $ Strict.readFile file >>= return . read
            
    classInfo <- maybe getClassInfo (\f -> print " (from dump)" >> readClassInfo f) reflectDump
    println """ Done (found #{show $ Map.size classInfo} classes)."""

    unless (null (opts `get` optSaveReflectDump)) $ io $ do
        file <- openFile (opts `get` optSaveReflectDump) WriteMode
        print """Saving dump of reflection info..."""
        hPutStr file (show classInfo)
        println " Done."
        hClose file

    return classInfo

-- This actually creates the Haskell module files for packages
generatePackageModules
    :: Options
    -> ClassInfo
    -> [String]
    -> Java ()
generatePackageModules opts info packages = do

    cwd <- io $ getCurrentDirectory
    print """Generating modules for #{show $ length packages} packages in #{cwd}..."""

    -- Create all the package files.
    -- These contain the Java classes as Haskell types.
    forM_ packages $ \packageName -> do
        let packageMod = info `packageModName` packageName
            dirName = strJoin [pathSeparator] (strSplitAll "." packageMod)

        -- Create the directory for the package
        io $ do 
            createDirectoryIfMissing True dirName

            -- Create the Haskell module file for the Java package
            file <- openFile (dirName ++ ".hs") WriteMode
            code <- gen info $ javaPackageModule packageName
            hPutStrLn file code
            hClose file

    when (opts `get` optCabalProject)
        $ writeCabalFile
            (opts `get` optProjectName)
            (opts `get` optProjectVersion)
            (opts `get` optCabalExtra)
            (opts `get` optDependencies)
            (map (info `packageModName`) packages)

    println " Done."


-- This actually creates the Haskell module files for classes
generateClassModules
    :: Options
    -> ClassInfo
    -> [String]
    -> Java ()
generateClassModules opts info classes = do

    cwd <- io $ getCurrentDirectory
    print """Generating modules for #{show $ length classes} classes in #{cwd}..."""

    -- Create all the class files.
    -- These contain the java methods as Haskell functions.
    forM_ classes $ \className -> do
        let fileName = strJoin [pathSeparator] (strSplitAll "." (info `classModName` className))
            dirName  = takeDirectory fileName

        io $ do
            createDirectoryIfMissing True dirName

            -- Create the Haskell module file for the Java class.
            file <- openFile (fileName ++ ".hs") WriteMode
            code <- gen info $ javaClassModule className
            hPutStrLn file code
            hClose file

            -- Create the interface Haskell module file for the Java class.
            file <- openFile (fileName ++ "__.hs") WriteMode
            code <- gen info $ javaClassModule' className
            hPutStrLn file code
            hClose file

    let modules = map (info `classModName`) classes
    when (opts `get` optCabalProject)
        $ writeCabalFile
            (opts `get` optProjectName)
            (opts `get` optProjectVersion)
            (opts `get` optCabalExtra)
            (opts `get` optDependencies)
            (modules ++ map (++ "__") modules)

    println " Done."


-- | Write a cabal project file. Can be used in any MonadIO, e.g. IO or Java or...
writeCabalFile :: MonadIO m =>
       String   -- project name
    -> String   -- project version
    -> String   -- extra 
    -> [String] -- dependencies
    -> [String] -- public module list
    -> m ()
writeCabalFile projectName projectVersion extra dependencies modules = liftIO $ do

    cabalFile <- openFile (projectName ++ ".cabal") WriteMode
    hPutStrLn cabalFile """
        name:           #{projectName}
        version:        #{projectVersion}
        cabal-version:  >= 1.8
        build-type:     Simple
        #{extra}
        Library
         build-depends:
          base >= 4 && < 5\
        #{concatMap (",\n  " ++) dependencies}
         exposed-modules:
          #{strJoin ",\n  " modules}
        """
    hClose cabalFile