packages feed

j2hs-0.9: 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 Bindings
import Utils

import Foreign.Java
import Foreign.Java.Utils
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.Bimap (Bimap)
import Data.Map (Map)
import Data.MultiMap (MultiMap)
import Data.Set (Set)
import qualified Data.Map as Map
import qualified Data.Set as Set
import qualified Data.MultiMap as MultiMap

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

import System.Directory
import System.FilePath
import System.IO hiding (print)
import System.Exit

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

j2hs
    :: Options
    -> [String]
    -> IO ()
j2hs opts args = do

    classpath <- mapM canonicalizePath $ opts `get` optClasspath
    targetDir <- return (opts `get` optTargetDirectory)
    let reflectDump_ = opts `get` optLoadReflectDump
    reflectDump <- if null reflectDump_
        then return Nothing
        else canonicalizePath reflectDump_ >>= return . Just

    let classpath' = concat $ L.intersperse ":" classpath
        indent     = ("  " ++)

    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:
                  #{targetDir}
                """
            createDirectoryIfMissing True targetDir
            setCurrentDirectory targetDir
            
        ifVerbose opts """
            Looking for Java classes:
            #{concatMap (\x -> "  " ++ x ++ "\n") args}"""

        -- Gather all classes that need to be translated
        print "Looking up classes..."
        classes  <- findClasses (opts `get` optSearchDepth) args

        println """ #{show $ length classes} found."""
        ifVerbose opts $
            unlines (map indent classes)

        -- Calculate the Haskell module names for packages and classes
        let 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

            -- 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 = packageModules `xGet` packageName

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

            -- The list of hidden modules
            hiddenModules = map (++ "__") $ Map.elems classModules

        classInfo <- gatherClassInfo opts reflectDump classModules

        findJavaCore classesByPackage classInfo

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

        generateModules opts classInfo classesByPackage classModules packageModules hiddenModules

    return ()

{-
findJavaCore2
    :: Map String JavaClass
    -> Java ()
findJavaCore2 classInfo = do

    let classes = 
-}

findJavaCore
    :: MultiMap String String
    -> Map String JavaClass
    -> Java ()
findJavaCore classesByPackage classInfo = do

    let packageDependencies :: Map String (Set String)
        packageDependencies = Map.map findDependencies (MultiMap.toMap classesByPackage)
          where
            findDependencies classes = Set.fromList packageDependencies
              where
                dependencies = concatMap (classDependencies . (classInfo Map.!)) classes
                packageDependencies = map (fst . splitClassName) dependencies

        resolve :: Set String -> Set String
        resolve = Set.fromList . concatMap (Set.toList . (packageDependencies Map.!)) . Set.toList

        fullPackageDependencies = Map.map (exhaustively resolve) packageDependencies

        coreJava = intersections (Map.elems fullPackageDependencies)

    println (show $ Set.size coreJava)
    println (Set.toList coreJava)

    forM_ (Map.toList fullPackageDependencies) $ \(pkg, deps) -> do
        let deps' = (deps Set.\\ coreJava)
        print """
            #{pkg}, #{show $ Set.size deps'} (#{show $ Set.size deps}) dependencies:
              """
        println $ Set.toList deps'


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

    print """Gathering refection information..."""
    let classNames = Map.keys classModules
        updName name clazz = (name, clazz { classModName = classModules Map.! name })

        getClassInfo :: Java (Map String JavaClass)
        getClassInfo = Map.fromList
            `fmap` map (uncurry updName)
            `fmap` zip classNames
            `fmap` everywhere (mkT (\(TyVar v) -> TyVar ('_':v)))
            `fmap` reflectClasses classNames

        readClassInfo :: String -> Java (Map String JavaClass)
        readClassInfo reflectFile = io $ do
            file <- openFile reflectFile ReadMode
            contents <- hGetContents file
            let contents' = read contents
            result <- contents' `seq` return contents'
            hClose file
            return result
            
    classInfo <- maybe getClassInfo (\f -> print " (from dump)" >> readClassInfo f) reflectDump
    println " Done."

    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 creates a graphviz file which can be used to
-- visualize inter-package dependencies
visualizeDependencies
    :: Map String (Set String)
    -> IO ()
visualizeDependencies packageDependencies = do

    file <- openFile "dependencies.neato" WriteMode
    hPutStrLn file """digraph G {\n  overlap = false;"""

    forM_ (Map.toList packageDependencies) $ \(pkg, deps) -> do
        forM_ (Set.toList deps) $ \dep -> do
            let augment = map (\x -> if x == '.' then '_' else x)
                pkg' = augment pkg
                dep' = augment dep
            hPutStr file """  #{pkg'} -> #{dep'}\n"""

    hPutStrLn file """}"""
    hClose file

-- This actually creates the Haskell module files
generateModules
    :: Options
    -> Map String JavaClass
    -> MultiMap String String
    -> Map String String
    -> Bimap CaseInsensitiveString CaseInsensitiveString
    -> [String]
    -> Java ()
generateModules opts classInfo classesByPackage classModules packageModules hiddenModules = do

    println """Generating modules for #{show $ Map.size classModules} classes in \
               #{show $ xSize packageModules} packages..."""

    -- A list of all modules
    let allModules = xKeysR packageModules ++ Map.elems classModules

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

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

            let classes = map (classInfo Map.!)
                              (MultiMap.lookup packageName classesByPackage)

            -- Create the Haskell module file for the Java package
            file <- openFile (dirName ++ ".hs") WriteMode
            hPutStrLn file
              $ printJavaPackageModule packageName packageMod classInfo classes
            hClose file

    -- Create all the class files.
    -- These contain the java methods as Haskell functions.
    forM_ (Map.toList classModules) $ \(clazzName, clazzMod) -> do
        let fileName = strJoin [pathSeparator] (strSplitAll "." clazzMod)
            clazz = classInfo Map.! clazzName

        io $ do
            -- Create the Haskell module file for the Java class.
            file <- openFile (fileName ++ ".hs") WriteMode
            hPutStrLn file $ printJavaClassModule clazz clazzMod classInfo
            hClose file

            -- Create the *hidden* Haskell module file for the Java class.
            file <- openFile (fileName ++ "__.hs") WriteMode
            hPutStrLn file $ printJavaClassModule' clazz clazzMod classInfo
            hClose file

    -- write Cabal file
    when (opts `get` optCabalProject) $ io $ do
        cabalFile <- openFile ((opts `get` optProjectName) ++ ".cabal") WriteMode
        hPutStrLn cabalFile """
            name:           #{opts `get` optProjectName}
            version:        #{opts `get` optProjectVersion}
            cabal-version:  >= 1.8
            build-type:     Simple
            
            Library {
             build-depends:
              base >= 4 && < 5,
              java-bridge
             exposed-modules:
              #{strJoin ",\n  " allModules}
             other-modules:
              #{strJoin ",\n  " hiddenModules} }
            """
        hClose cabalFile