WSLでJavaやPythonのソースコードからクラス図をDoxygen/Graphivzを使ってAnsible-playbookで生成する
はじめに
- WSL 上でソースコードからクラス図だけを抽出して一覧化し、ドキュメントとして落とすというニーズは、開発の現場ではときどき出てきますが、毎度手動でクラス図を手書きするのはかなり面倒です
- 面倒なのはすべて ansible でやりましょう
特徴
- Ansibleを使います。 Ansibleを使いたくないときは、以下の解説を見ながら手動で試してください
- Python と Javaの両方に対応しています。実際に両方を試して確認しています
- Doxygen/Graphivzで出力した HTML からクラス図のみを抽出するときに Python のコードを作っています
- 試してないですが、RHELでもそのまま使えると思います
本文
構成
- site_doxygen.yml
- JavaやPythonのソースコードからクラス図をDoxygen/Graphivzを使ってAnsible-playbookで生成する ansible-playbookです
- site_dixygen.pl
- Ansible-playbook で生成した HTML からクラス図のみを抽出して index2.html を生成します
- VBAを使ってExcelでクラス図の一覧を作ってヘッダやフッタを付けられるようにする
site_doxygen.yml
- 以下を ansible_doxygen.yml として保存します
- 環境に合わせて以下を変更します
- project_source_dir: ソースコードのフォルダ位置を指定してます
- project_output_dir: 出力先のフォルダ位置を指定します
- project_name: タイトルを指定します
- {{ project_output_dir }}/html/index.html にDoxygen/Graphivzを使ったリファレンスマニュアルを自動生成します
- hosts: localhost
vars:
# プロジェクトのソースパスとドキュメント出力先を指定
project_source_dir: "./src/main/java"
project_output_dir: "./doc_doxygen"
project_name: "Doxygen Project"
tasks:
- name: doxygenの存在をチェック
shell: command -v doxygen
register: doxygen_exists
failed_when: false
changed_when: false
- name: graphvizの存在をチェック
shell: command -v dot
register: dot_exists
failed_when: false
changed_when: false
- name: Javaの状態を表示
debug:
msg: dot_exists
- name: 存在してなかったらインストール
apt:
name:
- doxygen
- graphviz
state: present
become:
- "{{doxygen_exists.rc != 0 | bool}}"
- "{{dot_exists.rc != 0 | bool}}"
when:
- "{{doxygen_exists.rc != 0 | bool}}"
- "{{dot_exists.rc != 0 | bool}}"
- name: ドキュメント出力ディレクトリを作成
ansible.builtin.file:
path: "{{ project_output_dir }}"
state: directory
mode: '0755'
- name: Doxyfileの生成 設定済みのものがない場合
ansible.builtin.command:
cmd: doxygen -g
chdir: "."
creates: "Doxyfile"
- name: クラス図を有効にするためのDoxyfile設定変更
ansible.builtin.lineinfile:
path: "Doxyfile"
regexp: "{{ item.regexp }}"
line: "{{ item.line }}"
loop:
- { regexp: '^INPUT\s*=', line: 'INPUT = "{{ project_source_dir }}"' }
- { regexp: '^PROJECT_NAME\s*=', line: 'PROJECT_NAME = "{{ project_name }}"' }
- { regexp: '^OUTPUT_DIRECTORY\s*=', line: 'OUTPUT_DIRECTORY = {{ project_output_dir }}' }
- { regexp: '^HAVE_DOT\s*=', line: 'HAVE_DOT = YES' }
- { regexp: '^UML_LOOK\s*=', line: 'UML_LOOK = YES' }
- { regexp: '^EXTRACT_ALL\s*=', line: 'EXTRACT_ALL = YES' }
- { regexp: '^CLASS_GRAPH\s*=', line: 'CLASS_GRAPH = YES' }
- { regexp: '^COLLABORATION_GRAPH\s*=', line: 'COLLABORATION_GRAPH = YES' }
- { regexp: '^RECURSIVE\s*=', line: 'RECURSIVE = YES' }
- { regexp: '^CALLER_GRAPH\s*=', line: 'CALLER_GRAPH = YES' }
- { regexp: '^CLASS_DIAGRAMS\s*=', line: 'CLASS_DIAGRAMS = YES' }
- { regexp: '^EXTRACT_PRIVATE\s*=', line: 'EXTRACT_PRIVATE = YES' }
- name: Doxygenを実行してドキュメントを生成
ansible.builtin.command:
cmd: doxygen Doxyfile
chdir: "."
register: doxygen_result
- name: Doxygenからクラス図を抽出する
ansible.builtin.command:
cmd: python3 site_doxygen.py
chdir: "."
- name: 実行結果の表示
ansible.builtin.debug:
msg: "ドキュメントが {{ project_output_dir }}/html/index.html に生成されました。"
when: doxygen_result.rc == 0
site_doxygen.py
- 次に以下を ansible_doxygen.py として保存します
- ソースコード中にクラス図を抽出するPythonコードです
- ターゲットとなる site_doxygen.yml を指定しているので、そこだけ注意が必要です
- {{ project_output_dir }}/html/index2.html に抽出したクラス図のリストを表示します
import re
import sys
base = None
project_name = ""
with open('site_doxygen.yml', 'r', encoding='utf-8') as f:
for line in f:
match = re.search('project_output_dir\\s*:\\s*\"([^\"]+)', line)
if match:
base = match.group(1)
match = re.search('project_name\\s*:\\s*\"([^\"]+)', line)
if match:
project_name = match.group(1)
if base is None:
sys.exit(-1)
uml = base + "/html/annotated.html"
urls = []
with open(uml, 'r', encoding='utf-8') as f:
for line in f:
match = re.search('<a\\s+class="([^\"]+)\"\\s+href=\"(class[^\"]+)', line)
if match:
urls.append(match.group(2))
imgs = []
for url in urls:
url = base + "/html/" + url
with open(url, 'r', encoding='utf-8') as f:
title = ""
for line in f:
match = re.search('^(Inheritance|Collaboration)\\s+[^<>]+', line)
if match:
title = match.group()
match = re.search('<img\\s+src=\"([^\"]+)', line)
if match:
imgs.append({"title": title, "utl": match.group(1)})
break
uml = base + "/html/index2.html"
with open(uml, mode='w', encoding="utf-8") as f:
f.write("<html lang=\"jp\">\n")
f.write("<head>\n")
f.write("<meta http-equiv=\"Content-Type\" content=\"text/html; charset=utf-8\">\n")
f.write("</head>\n")
f.write("<body>\n")
f.write(f"<H1>{project_name}</H1>")
for key in imgs:
f.write(f"<H2>{key["title"]}</H2>")
f.write(f"<img src=\"{key["utl"]}\" />\n") # height=\"100%\"
f.write("<mbp:pagebreak/>\n")
f.write("</body>\n")
f.write("</html>\n")
#
動かし方
- 以下を実行します
- apt install をrootで実行するために -K (大文字のK)オプションを指定しています
ansible-playbook -K ansible_doxygen.yml
- すでにdoxygenとgraphvizがインストール済であれば rootは不要なので-K (大文字のK)オプションは不要です
ansible-playbook ansible_doxygen.yml
出力例
解説
- apt で doxygen と graphvizを生成します
- こう書くとインストール済のときroot不要となりより安全です
- name: doxygenの存在をチェック
shell: command -v doxygen
register: doxygen_exists
failed_when: false
changed_when: false
- name: graphvizの存在をチェック
shell: command -v dot
register: dot_exists
failed_when: false
changed_when: false
- name: Javaの状態を表示
debug:
msg: dot_exists
- name: 存在してなかったらインストール
apt:
name:
- doxygen
- graphviz
state: present
become:
- "{{doxygen_exists.rc != 0 | bool}}"
- "{{dot_exists.rc != 0 | bool}}"
when:
- "{{doxygen_exists.rc != 0 | bool}}"
- "{{dot_exists.rc != 0 | bool}}"
- ドキュメント用のフォルダを作成します
-
mkdirと同等です
- name: ドキュメント出力ディレクトリを作成
ansible.builtin.file:
path: "{{ project_output_dir }}"
state: directory
mode: '0755'
- ドキュメント用のフォルダを作成します
- これによりカレントフォルダに DoxyFileが生成されます
-
doxygen -gと同等です
- name: Doxyfileの生成 設定済みのものがない場合
ansible.builtin.command:
cmd: doxygen -g
chdir: "."
creates: "Doxyfile"
- 生成された Doxyfile を Java や Python 用にクラス図を出力するために書き換えます
-
vi Doxyfileによる手動修正と同等です - Ansibleを使わないときは手動でちまちま探して直さないといけないですが、Ansibleはこのような書き方でまとめて直せます
- name: クラス図を有効にするためのDoxyfile設定変更
ansible.builtin.lineinfile:
path: "Doxyfile"
regexp: "{{ item.regexp }}"
line: "{{ item.line }}"
loop:
- { regexp: '^INPUT\s*=', line: 'INPUT = "{{ project_source_dir }}"' }
- { regexp: '^PROJECT_NAME\s*=', line: 'PROJECT_NAME = "{{ project_name }}"' }
- { regexp: '^OUTPUT_DIRECTORY\s*=', line: 'OUTPUT_DIRECTORY = {{ project_output_dir }}' }
- { regexp: '^HAVE_DOT\s*=', line: 'HAVE_DOT = YES' }
- { regexp: '^UML_LOOK\s*=', line: 'UML_LOOK = YES' }
- { regexp: '^EXTRACT_ALL\s*=', line: 'EXTRACT_ALL = YES' }
- { regexp: '^CLASS_GRAPH\s*=', line: 'CLASS_GRAPH = YES' }
- { regexp: '^COLLABORATION_GRAPH\s*=', line: 'COLLABORATION_GRAPH = YES' }
- { regexp: '^RECURSIVE\s*=', line: 'RECURSIVE = YES' }
- { regexp: '^CALLER_GRAPH\s*=', line: 'CALLER_GRAPH = YES' }
- { regexp: '^CLASS_DIAGRAMS\s*=', line: 'CLASS_DIAGRAMS = YES' }
- { regexp: '^EXTRACT_PRIVATE\s*=', line: 'EXTRACT_PRIVATE = YES' }
- Doxygen/Graphivz をつかってDoxygileからクラス図を生成します
-
doxygen Doxyfileと同等です
- name: Doxygenを実行してドキュメントを生成
ansible.builtin.command:
cmd: doxygen Doxyfile
chdir: "."
register: doxygen_result
- Doxygen/Graphivz の出力は余分なものも含まれているので、以下によりクラス図を抽出します
-
python3 site_doxygen.pyと同等です
- name: Doxygenからクラス図を抽出する
ansible.builtin.command:
cmd: python3 site_doxygen.py
chdir: "."
- 結果を表示します
- name: 実行結果の表示
ansible.builtin.debug:
msg: "ドキュメントが {{ project_output_dir }}/html/index.html に生成されました。"
when: doxygen_result.rc == 0
VBAを使ってExcelでクラス図の一覧を作ってヘッダやフッタを付けられるようにする
- 上記では、site_doxygen.py を使ってクラス図の一覧をHTMLで作り、それをpdf化することでクラス図の一覧を印刷できるようにしました
- しかし、site_doxygen.pyで出力されるindex.htmlはヘッダが付かなかったり、改行がうまく行かなかったり、画像がPDFからはみ出たり、ページ番号が付かないといった事象がおきます
- そこで、VBAを使ってヘッダやフッタを付けられるようにしたのが、以下となります
- 簡単にできるかと思ったら結構長く…
Public FSO As Object
Sub ボタン_Click()
If FSO Is Nothing Then
Set FSO = CreateObject("Scripting.FileSystemObject")
End If
Dim filePath As String
' ファイル選択ダイアログを表示
filePath = Application.GetOpenFilename(title:="Doxygenのindex.htmを指定してください", FileFilter:="Doxygen File,*.html")
' キャンセルされた場合は処理を終了
If filePath = "False" Then
MsgBox "キャンセルされました"
Exit Sub
End If
Dim wb As Workbook
Set wb = Workbooks.Add
Dim ws As Worksheet
Set ws = wb.Worksheets(1)
ws.Name = "index"
Dim folderPath As String
folderPath = FSO.GetParentFolderName(filePath)
filePath = folderPath & "\annotated.html"
Dim re As New RegExp
re.Pattern = "\s+<div\s+id=""projectname"">([\s\S]+)"
re.Global = True
Dim re2 As New RegExp
re2.Pattern = "<a\s+class=""[^""]+""\s+href=""(class[^""]+).html"
re2.Global = True
Dim re3 As New RegExp
re3.Pattern = "^\d+"
re3.Global = True
Dim projectname As String
projectname = ""
Dim rowNum As Long
rowNum = 2
Set ts = FSO.OpenTextFile(filePath, 1)
Do Until ts.AtEndOfStream
Dim mc As MatchCollection
Dim m As Match
lineStr = ts.ReadLine ' 1行読み込み
'
Set mc = re.Execute(lineStr)
For Each m In mc
projectname = m.SubMatches(0)
ws.Range("A1").Value = projectname
Next
'
Set mc = re2.Execute(lineStr)
For Each m In mc
Dim str As String
str = m.SubMatches(0)
Cells(rowNum, 2).Value = str & ".html"
'
str = Mid(str, InStrRev(str, "_") + 1)
str = re3.Replace(str, "")
Cells(rowNum, 1).Value = str
'
rowNum = rowNum + 1
Next
Loop
ts.Close
rowNum = 2
Do Until ws.Cells(rowNum, 1).Value = ""
str = ws.Cells(rowNum, 1).Value
Dim classWs As Worksheet
Set classWs = SheetGet(wb, str)
str = folderPath & "\" & ws.Cells(rowNum, 2).Value
ws.Cells(rowNum, 2).Value = CrateClassData(classWs, folderPath, str, projectname)
' 行を増やす
rowNum = rowNum + 1
Loop
' 全ワークシートを選択
wb.Worksheets.Select
' アクティブウィンドウを改ページプレビューに設定
ActiveWindow.View = xlPageBreakPreview
' 表題を入れる
Application.DisplayAlerts = False
ws.Range("A1:B1").Merge
Application.DisplayAlerts = True
ws.Range("A1").Value = "Table of Contents"
ws.Range("A1").Font.Bold = True
ws.Range("A1").Font.Size = 16
' 1行で1ページにする
ws.PageSetup.Zoom = False
ws.PageSetup.FitToPagesWide = False
ws.PageSetup.FitToPagesWide = 1
' 列幅を文字数に合わせる
ws.Cells.EntireColumn.AutoFit
' 先頭をアクティブページにする
wb.Worksheets(1).Activate
End Sub
' シートの存在を検知する
Function SheetExists(wb As Workbook, sheetName As String) As Boolean
Dim ws As Worksheet
SheetExists = False
For Each ws In wb.Sheets
' 大文字小文字を区別せず比較
If UCase(ws.Name) = UCase(sheetName) Then
SheetExists = True
Exit Function
End If
Next ws
End Function
' オリジナルなシートを取得する
Function SheetGet(wb As Workbook, sheetName As String) As Worksheet
Dim i As Long
Dim ws As Worksheet
i = 0
Do
If i = 0 Then
If SheetExists(wb, sheetName) = False Then
Set ws = wb.Worksheets.Add(After:=wb.Worksheets(wb.Worksheets.Count))
ws.Name = sheetName
Set SheetGet = ws
Exit Function
End If
Else
Dim str As String
str = sheetName & "(" & CStr(i) & ")"
If SheetExists(wb, str) = False Then
Set ws = wb.Worksheets.Add(After:=wb.Worksheets(wb.Worksheets.Count))
ws.Name = str
Set SheetGet = ws
Exit Function
End If
End If
i = i + 1
Loop
End Function
' クラス図を表示する
Function CrateClassData(ws As Worksheet, folderPath As String, filePath As String, projectname As String) As String
Dim re As New RegExp
re.Pattern = "^(Inheritance|Collaboration)\s+[^<>]+"
re.Global = True
Dim re2 As New RegExp
re2.Pattern = "<img\s+src=""([^""]+)"
re2.Global = True
Dim title As String
title = ""
Set ts = FSO.OpenTextFile(filePath, 1)
Do Until ts.AtEndOfStream
Dim mc As MatchCollection
Dim m As Match
lineStr = ts.ReadLine ' 1行読み込み
Set mc = re.Execute(lineStr)
For Each m In mc
title = m.Value
Next
title = Replace(title, "<", "<")
title = Replace(title, ">", ">")
Set mc = re2.Execute(lineStr)
For Each m In mc
Dim str As String
str = folderPath & "\" & m.SubMatches(0)
Call InsertPictureToFit(ws, str, projectname)
ws.Range("A1").Value = title
Exit Do
Next
Loop
ts.Close
CrateClassData = title
End Function
' 画像を挿入して1ページに収める
Sub InsertPictureToFit(ws As Worksheet, imgPath As String, projectname As String)
Dim pic As Shape
Dim targetRange As Range
Set targetRange = ws.Range("A1")
Set pic = ws.Shapes.AddPicture( _
Filename:=imgPath, _
LinkToFile:=msoFalse, _
SaveWithDocument:=msoTrue, _
Left:=targetRange.Left, _
Top:=targetRange.Top, _
Width:=-1, _
Height:=-1)
pic.LockAspectRatio = msoTrue
If pic.Width > ws.Cells(1, 1).Width * 10 Then
pic.Width = 500
End If
If pic.Height > 500 Then
pic.Height = 500
End If
' 一行追加する
ws.Rows(1).Insert
' 必要に応じて印刷設定
ws.PageSetup.Zoom = False
ws.PageSetup.FitToPagesWide = 1
ws.PageSetup.FitToPagesTall = 1
ws.PageSetup.RightHeader = projectname
ws.PageSetup.CenterFooter = "&P / &N"
End Sub
