3ヶ月くらい前に書いたclass図作成プログラム

ただいまハッカソン中(`・ω・´)ハッカソン中にblogを書くという変な行動にでてみるw

だいぶ前に、作成したSVGのものは載せたけど、ソースコード載せてなかったのでとりあえずのせとく。
今日はこれをいろいろいじってみるか => 結局いじらずに違うことやりましたすいません(´・ω・`)

ちなみに、自分でも中の構造がどうなっているのかだいぶ忘れてますが・・・もう汚いまま載せておきます。
アルゴリズム汚いし、ちょっとバグってるしw
あと、ちなみに、ファイルに書き出してるところとか自作のutility使ってるから、ここに貼ったものだけじゃ動きません(´・ω・`)単なる自分のメモです

はたして3ヶ月前より成長してるだろうか

package test

import scala.xml._

/**
 * scalaのクラス図作成?
 * @since 2010/07/25
 */
object ClassDiagram {
  import reflectionTest.getAllClassAndTrait
  
  /**
   * @param clazz 自身のclass
   * @param level 自身のsubclassが多いほど大きくなる
   * @param parents 直接の親のリスト
   */
  private case class ClassNode( clazz:Class[_],var level:Int,parents:Class[_] * ) extends  math.Ordered[ClassNode]{
    import ClassNode._
    
    /** 間接的なものも含めた、すべての親 */
    lazy val allParents = getAllClassAndTrait(this.clazz)
    
    var yoko = 0

    @inline private def S(e:Any):String = e.toString
 
    /**
     * 比べた結果を返すというより、内部状態を変化させるため
     */
    override def compare(that: ClassNode): Int = {
      if( this.parents.contains(that.clazz) ){
        if( that.parents.contains(this.clazz) ){
          throw new Error("循環参照? "+this.clazz+" "+that.clazz)
        }else{
          if(that.level <= this.level){
            that.level = this.level + 1
          }
          1
        }
      }else{
        if( that.parents.contains(this.clazz) ){
          if(that.level >= this.level){
            this.level = that.level + 1
          }
          -1
        }else{
          if(this.allParents.contains(that.clazz)){
            if(that.level <= this.level){
              that.level = this.level + 1
            }
            1
          }else if(that.allParents.contains(this.clazz)){
            if(that.level >= this.level){
              this.level = that.level + 1
            }
            -1
          }else{
            0
          }
        }
      }
    }
    
    private lazy val fullName = clazz.getName
      
    private lazy val url = {
        val path = fullName.replace(".","/")
        if( fullName.startsWith("scala") ){
          "http://www.scala-lang.org/archives/downloads/distrib/files/nightly/docs/library/"+path+".html"
//          "http://www.scala-lang.org/api/current/"+path+".html"//普通のpath
        }else if(fullName.startsWith("java") ){
          "http://java.sun.com/javase/ja/6/docs/ja/api/"+path+".html"
        }else{
          ""
        }
    }
    
    def toXml:Node = {
      val packageFontSize = 10
      val maxFontSize = 18
      val baseX = yoko * w
      val baseY = level * h
      val middleX = baseX + (recW/2)
      val simpleName = clazz.getSimpleName
      val tmpSize = (recW*2)/(simpleName.length)
      val fontSize = if( tmpSize < maxFontSize ){ tmpSize }else{ maxFontSize }
      
      <g>
          <a xlink:href={url} target="_blank" >
            <rect x={S{ baseX }} y={S{baseY}} width={S{recW}} height ={S{recH}} fill={if(clazz.isInterface)"#799F5A"else"#7996AC"} stroke="black" stroke-width="2">
            </rect>
            <text x={S{ baseX +5}} y={S{baseY + 15}} font-size={S{packageFontSize}} >{ fullName.replace(simpleName,"") }</text>
            <text x={S{ baseX +5}} y={S{baseY + packageFontSize + 24}} font-size={S{fontSize}} >{ simpleName }</text>
          </a>
        {for{ p <- parents
             if ! exceptList.contains(p)
            }yield{
          val s = allClassNodes.find{ _.clazz == p }.get
          <line x1={S{ middleX }} y1={S{baseY + recH}} x2={S{ s.yoko * w + (recW/2)}} y2={S{s.level * h }} stroke="black" stroke-width="1"/>
        }}
      </g>
    }
  }
  
  private object ClassNode{
    var allClassNodes:List[ClassNode] = Nil
    private val w = 120//基準位置の横幅
    private val h = 200//基準位置の縦幅
    private val recW = w - 20//四角形の幅
    private val recH = h - 160//四角形の高さ
    
    //線をひくのをやめるやつ
    val exceptList = List("scala.ScalaObject","java.lang.Object").map{ Class.forName }
  }
  
  /**
   * x > 0 かつ y > 0の引数が渡ってきたときに、
   * xについて単射になるように、かつ大きくなりすぎないように返す
   */
  private def rand(x:Int,y:Int):Int = {
     ( x * ( y%2 + 1 )) + y%3
  }
  

  
  /**
   * 実際のインスタンスから作成
   * @param fileName 保存するファイル名
   * @param objList 作成元のオブジェクト
   */
  def createClassDiagramByObj(fileName:String)(objList: AnyRef *){
    create( fileName , objList.map{_.getClass} )
  }
  
  /**
   * クラスの完全修飾名から作成
   * @param fileName 保存するファイル名
   * @param classNames 作成するクラス名
   */
  def createClassDiagramByName(fileName:String)(classNames: String * ){
    create(fileName, classNames.map{ Class.forName(_) } )
  }
  
  /**
   * 
   */
  private[this] def create(fileName:String, classes: Traversable[Class[_]] ) = {
        
    import collection.{ mutable => mu }
    
    val result = 
    sortByInheritance{
      classes.foldLeft(List[ClassNode]()){ (list,clazz) =>
        makeClassNodes( clazz ) ::: list
      }.distinct
    }

    //デバック用表示
    result.foreach{ case (n,list) =>
      println( n ,list.size,list.map{ _.clazz.getSimpleName } )
    }
    
    //横の位置計算して決めて、ClassNodeオブジェクトのフィールドに保存
    ClassNode.allClassNodes = 
    result.flatMap{ case (_,list) => 
        list.zipWithIndex.map{ case (data,n) => data.yoko = n * (((data.level*1.6).asInstanceOf[Int])%2 + 1 ) }
//      list.zipWithIndex.map{ case (data,n) => data.yoko = rand(n,data.level) }
      list
    }
    

    
    //xmlに変換
    val xmlNodeList = ClassNode.allClassNodes.filterNot{x => ClassNode.exceptList.contains(x)}.map{  _.toXml }

    saveXML(fileName,xmlNodeList)
  }
  
  private def saveXML(fileName:String,xmlNodeList:List[scala.xml.Node]){
    val width = 25000
    val height = 2000
    
    //ファイル保存 TODO 文字列で書き込むのではなく、XMLのオブジェクトにして、xml用の関数つかう
    utility.FileUtilO.writeFile(fileName,"Shift-JIS")(
      """<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.0//EN" "http://www.w3.org/TR/2000/CR-SVG-20001102/DTD/svg-20001102.dtd">
      <svg xmlns="http://www.w3.org/2000/svg" 
     xmlns:xlink="http://www.w3.org/1999/xlink"
     id="body" width="""" + width + """" height=""""+ height +"""" viewBox="-10 -10 """+ width + " " + height + """">
     """
     + xmlNodeList.mkString(" "," "," ") + 
     "</svg>"
     )
    //saveSVG("""C:\Users\admin\Desktop\hoge2.svg""")(Elem(null,"g",null,xml.TopScope,xmlNodeList : _*))
    println("おわり")
    
  }

  
  /**
   * (ソートしてない)ClassNodeのList作成
   */
  private def makeClassNodes(clazz:Class[_]):List[ClassNode] = {
    getAllClassAndTrait(clazz).map{ 
      x => ClassNode( x ,0,  x.getInterfaces : _*) 
    }
  }

  /**
   * ClassNodeのListをソートし、グループ分け
   * (それぞれのlevelの値を変化させる)
   */
  private def sortByInheritance(classList:List[ClassNode]):List[Pair[Int,List[ClassNode]]] = {

    /** ローカルのヘルパー関数,再帰 */
    @scala.annotation.tailrec
    def loop(m:List[ClassNode]){
      val oldMax = m.foldLeft(0){ (x,y) => x max y.level }
      for(x <- m;y <- m){//総当りで呼ぶ
        x.compare(y)
      }
      println("loop" )
      val newMax = m.foldLeft(0){ (x,y) => x max y.level }
      if(oldMax != newMax){
        loop(m)
      }
    }
    
    loop(classList)
    
    classList.groupBy{ _.level }.toList.sortBy(_._1)
    
  }
 
  
}
/**
 * @since 2010/07/24 14:40:27
 */
package test

/**
 * @since 2010/07/24 14:40:27
 * Listとかの階層調べるため
 */
object reflectionTest {

  def main(args: Array[String]): Unit = sub1  
  import java.lang.Class
  
  def sub1{
    
    val m = getAllClassAndTrait(List().getClass).map{x => (x,x.getInterfaces) }
    
//   val hoge = groupByInterfaseCount(getAllInterfaces(1 to 10))
//    hoge.foreach{ case (i,list) => println(i,list.size,list.mkString("[ "," , "," ]")) }

  }
  

  
  
  //interfaseの数によって、グループ分けして返す
  val groupByInterfaseCount = { classList:List[Class[_]] =>
    
    val classNames = classList.map{c => (c, c.getSimpleName) }
    
    //class名が重複しているclassのリスト
    val  overlap =
      
      classNames.map{ case (c,name) =>
        
        (classNames.count{ case (_,y) => name == y},c)
        
      }.filterNot{ case (count,name) =>
        count == 1
      }.map{ case (_,name) => name }
    
    println(overlap.map{_.getSimpleName}.distinct);println
    
    classList.groupBy{
      c => c.getInterfaces.size
    }.map{ case (i,list) =>
      (i,
        list.map{ e =>
          if(overlap.contains(e)){
            //クラス名と、そのひとつ外側のパッケージ名
            e.toString.split("""\.""").takeRight(2).reduceLeft(_+"."+_)
          }else{
            //クラス名のみ
            e.getSimpleName
          }
        }
      )
    }.toList.sortBy{ case (i,_) => i }
  }
  

  def getSuperClasses(clazz:Class[_]):List[Class[_]] = {
 
    @scala.annotation.tailrec
    def sub( o:Class[_] , result:List[Class[_]] ): List[Class[_]] = {
      val superClass = o.getSuperclass
      if(superClass == null){
        result
      }else{
        sub(superClass , superClass::result )
      }
    }
    
    sub(clazz,List(clazz))
  }
  
  //階層をたどっていってすべての親インターフェイスを探す
  def getAllClassAndTrait(clazz:Class[_]):List[Class[_]] = {
  
    def sub( c:Class[_] , result:List[Class[_]] ): List[Class[_]] = {
      
      val interfaces = c.getInterfaces.toList
      
      if( interfaces.size == 0 ){
        result
      }else{
        interfaces.flatMap{ i => sub( i , i::result ) }
      }
    }
    
    getSuperClasses(clazz).flatMap{x => sub(x,List(x)) }.distinct - clazz
    
  }

}