neo_doxygen: Remove an old TODO.
[nit.git] / contrib / neo_doxygen / src / neo_doxygen.nit
1 # This file is part of NIT ( http://www.nitlanguage.org ).
2 #
3 # Licensed under the Apache License, Version 2.0 (the "License");
4 # you may not use this file except in compliance with the License.
5 # You may obtain a copy of the License at
6 #
7 # http://www.apache.org/licenses/LICENSE-2.0
8 #
9 # Unless required by applicable law or agreed to in writing, software
10 # distributed under the License is distributed on an "AS IS" BASIS,
11 # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 # See the License for the specific language governing permissions and
13 # limitations under the License.
14
15 # Doxygen XML to Neo4j.
16 #
17 # Converts a Doxygen XML output into a model in Neo4j that is readable by the
18 # `nx` tool.
19 module neo_doxygen
20
21 import model
22 import doxml
23 import console
24 import opts
25
26 # An importation task.
27 class NeoDoxygenJob
28 var client: Neo4jClient
29 var model: ProjectGraph is noinit
30
31 # How many operation can be executed in one batch?
32 private var batch_max_size = 1000
33
34 private var save_cursor: String = (new TermSaveCursor).to_s
35
36 # Escape control sequence to reset the current line.
37 private var reset_line: String = "{new TermRestoreCursor}{new TermEraseDisplayDown}"
38
39 # Generate a graph from the specified project model.
40 #
41 # Parameters:
42 #
43 # * `name`: project name.
44 # * `dir`: Doxygen XML output directory path.
45 # * `source`: The language-specific logics to use.
46 fun load_project(name: String, dir: String, source: SourceLanguage) do
47 check_name name
48 model = new ProjectGraph(name)
49 var reader = new CompoundFileReader(model, source)
50 # Queue for sub-directories.
51 var directories = new Array[String]
52 var file_count = 0
53
54 if dir == "" then
55 sys.stdout.write "Reading the current directory... "
56 else
57 sys.stdout.write "Reading {dir}... "
58 end
59 loop
60 for f in dir.files do
61 var path = dir/f
62 if path.file_stat.is_dir then
63 directories.push(path)
64 else if f.has_suffix(".xml") and f != "index.xml" then
65 reader.read(path)
66 file_count += 1
67 end
68 end
69 if directories.length <= 0 then break
70 dir = directories.pop
71 end
72 print "Done."
73 if file_count < 2 then
74 print "{file_count} file read."
75 else
76 print "{file_count} files read."
77 end
78 end
79
80 # Check the project’s name.
81 private fun check_name(name: String) do
82 assert name_valid: not name.chars.first.is_upper else
83 sys.stderr.write("{sys.program_name}: The project’s name must not" +
84 " begin with an upper case letter. Got `{name}`.\n")
85 end
86 var query = new CypherQuery.from_string("match n where \{name\} in labels(n) return count(n)")
87 query.params["name"] = name
88 var data = client.cypher(query).as(JsonObject)["data"]
89 var result = data.as(JsonArray).first.as(JsonArray).first.as(Int)
90 assert name_unused: result == 0 else
91 sys.stderr.write("{sys.program_name}: The label `{name}` is already" +
92 " used in the specified graph.\n")
93 end
94 end
95
96 # Save the graph.
97 fun save do
98 sys.stdout.write "Linking nodes...{save_cursor} "
99 model.put_edges
100 print "{reset_line} Done."
101 var nodes = model.all_nodes
102 sys.stdout.write "Saving {nodes.length} nodes...{save_cursor} "
103 push_all(nodes)
104 var edges = model.all_edges
105 sys.stdout.write "Saving {edges.length} edges...{save_cursor} "
106 push_all(edges)
107 end
108
109 # Save `neo_entities` in the database using batch mode.
110 private fun push_all(neo_entities: Collection[NeoEntity]) do
111 var batch = new NeoBatch(client)
112 var len = neo_entities.length
113 var sum = 0
114 var i = 1
115
116 for nentity in neo_entities do
117 batch.save_entity(nentity)
118 if i == batch_max_size then
119 do_batch(batch)
120 sum += batch_max_size
121 sys.stdout.write("{reset_line} {sum * 100 / len}% ")
122 batch = new NeoBatch(client)
123 i = 1
124 else
125 i += 1
126 end
127 end
128 do_batch(batch)
129 print("{reset_line} Done.")
130 end
131
132 # Execute `batch` and check for errors.
133 #
134 # Abort if `batch.execute` returns errors.
135 private fun do_batch(batch: NeoBatch) do
136 var errors = batch.execute
137 if not errors.is_empty then
138 for e in errors do sys.stderr.write("{sys.program_name}: {e}\n")
139 exit(1)
140 end
141 end
142 end
143
144 # The main class.
145 class NeoDoxygenCommand
146
147 # Invalid arguments
148 var e_usage = 64
149
150 # Available options for `--src-lang`.
151 var sources = new HashMap[String, SourceLanguage]
152
153 # The synopsis.
154 var synopsis: String = "[--dest <url>] [--src-lang <lang>]\n" +
155 " [--] <project_name> <doxml_dir>"
156
157 # The synopsis for the help page.
158 var help_synopsis = "[-h|--help]"
159
160 # The default destination.
161 var default_dest = "http://localhost:7474"
162
163 # Processes the options.
164 var option_context = new OptionContext
165
166 # The `--src-lang` option.
167 var opt_src_lang: OptionEnum is noinit
168
169 # The `--dest` option.
170 var opt_dest: OptionString is noinit
171
172 # The `-h|--help` option.
173 var opt_help: OptionBool is noinit
174
175 init do
176 sources["any"] = new DefaultSource
177 sources["java"] = new JavaSource
178
179 var prefix = new OptionText("""
180 {{{"NAME".bold}}}
181 {{{sys.program_name}}} — Doxygen XML to Neo4j.
182
183 {{{"SYNOPSIS".bold}}}
184 {{{sys.program_name}}} {{{synopsis}}}
185 {{{sys.program_name}}} {{{help_synopsis}}}
186
187 {{{"DESCRIPTION".bold}}}
188 Convert a Doxygen XML output into a model in Neo4j that is readable by the
189 `nx` tool.
190
191 {{{"ARGUMENTS".bold}}}
192 <project_name> The internal name of the project. Must the same name as the
193 one specified to the `nx` tool. Must not begin by an upper
194 case letter.
195
196 <doxml_dir> The directory where the XML documents generated by Doxygen are
197 located.
198
199 {{{"OPTIONS".bold}}}
200 """)
201 option_context.add_option(prefix)
202
203 opt_dest = new OptionString("The URL of the destination graph. `{default_dest}` by default.",
204 "--dest")
205 opt_dest.default_value = default_dest
206 option_context.add_option(opt_dest)
207
208 opt_help = new OptionBool("Show the help (this page).",
209 "-h", "--help")
210 option_context.add_option(opt_help)
211
212 var keys = new Array[String].from(sources.keys)
213 opt_src_lang = new OptionEnum(keys,
214 "The programming language to assume when processing chunk in the declarations left as-is by Doxygen. Use `any` (the default) to disable any language-specific processing.",
215 keys.index_of("any"), "--src-lang")
216 option_context.add_option(opt_src_lang)
217 end
218
219 # Start the application.
220 fun main: Int do
221 if args.is_empty then
222 show_help
223 return e_usage
224 end
225 option_context.parse(args)
226
227 var errors = option_context.get_errors
228 var rest = option_context.rest
229
230 if errors.is_empty and not opt_help.value and rest.length != 2 then
231 errors.add "Unexpected number of additional arguments. Expecting 2; got {rest.length}."
232 end
233 if not errors.is_empty then
234 for e in errors do print_error(e)
235 show_usage
236 return e_usage
237 end
238 if opt_help.value then
239 show_help
240 return 0
241 end
242
243 var source = sources[opt_src_lang.value_name]
244 var dest = opt_dest.value
245 var project_name = rest[0]
246 var dir = rest[1]
247 var neo = new NeoDoxygenJob(new Neo4jClient(dest or else default_dest))
248
249 neo.load_project(project_name, dir, source)
250 neo.save
251 return 0
252 end
253
254 # Show the help.
255 fun show_help do
256 option_context.usage
257 end
258
259 # Show the usage.
260 fun show_usage do
261 sys.stderr.write "Usage: {sys.program_name} {synopsis}\n"
262 sys.stderr.write "For details, run `{sys.program_name} --help`.\n"
263 end
264
265 # Print an error.
266 fun print_error(e: String) do
267 sys.stderr.write "{sys.program_name}: {e}\n"
268 end
269 end
270
271 # Add handling of multi-line descriptions.
272 #
273 # Note: The algorithm is naive and do not handle internationalisation and
274 # escape sequences.
275 redef class Option
276
277 redef fun pretty(off) do
278 var s = super
279
280 if s.length > 80 and off < 80 then
281 var column_length = 80 - off
282 var left = 0
283 var right = 80
284 var buf = new FlatBuffer
285 var prefix = "\n{" " * off}"
286
287 loop
288 while right > left and s.chars[right] != ' ' do
289 right -= 1
290 end
291 if left == right then
292 buf.append s.substring(left, column_length)
293 right += column_length
294 else
295 buf.append s.substring(left, right - left)
296 right += 1
297 end
298 buf.append prefix
299 left = right
300 right += column_length
301 if right >= s.length then break
302 end
303 buf.append s.substring_from(left)
304 buf.append "\n"
305 return buf.to_s
306 else
307 return "{s}\n"
308 end
309 end
310 end
311
312 exit((new NeoDoxygenCommand).main)