# See the License for the specific language governing permissions and
# limitations under the License.
-# Doxygen XML to Neo4j
+# Doxygen XML to Neo4j.
#
-# ## Synopsis
-#
-# neo_doxygen project_name xml_output_dir [neo4j_url]
-#
-# ## Description
-#
-# Convert a Doxygen XML output into a model in Neo4j that is readable by the
+# Converts a Doxygen XML output into a model in Neo4j that is readable by the
# `nx` tool.
-#
-# ## Arguments
-#
-# * project_name: The internal name of the project. Must the same name as the
-# one specified to the `nx` tool.
-#
-# * xml_output_dir: The directory where the XML documents generated by Doxygen
-# are located.
-#
-# * neo4j_url: The URL of the instance of Neo4j to use.
-# `http://localhost:7474` by default.
module neo_doxygen
import model
import doxml
+import graph_store
+import console
+import opts
# An importation task.
-class NeoDoxygen
- var client: Neo4jClient
+class NeoDoxygenJob
+
+ # The storage medium to use.
+ var store: GraphStore
+
+ # The loaded project graph.
var model: ProjectGraph is noinit
- # How many operation can be executed in one batch?
- private var batch_max_size = 1000
+ # Escape control sequence to save the cursor position.
+ private var term_save_cursor: String = (new TermSaveCursor).to_s
+
+ # Escape control sequence to rewind to the last saved cursor position.
+ private var term_rewind: String = "{new TermRestoreCursor}{new TermEraseDisplayDown}"
# Generate a graph from the specified project model.
#
#
# * `name`: project name.
# * `dir`: Doxygen XML output directory path.
- fun put_project(name: String, dir: String) do
+ # * `source`: The language-specific logics to use.
+ fun load_project(name: String, dir: String, source: SourceLanguage) do
+ check_name name
model = new ProjectGraph(name)
- # TODO Let the user select the language.
- var reader = new CompoundFileReader(model, new JavaSource)
+ var reader = new CompoundFileReader(model, source)
# Queue for sub-directories.
var directories = new Array[String]
+ var file_count = 0
- if dir.length > 1 and dir.chars.last == "/" then
- dir = dir.substring(0, dir.length - 1)
+ if dir == "" then
+ printn "Reading the current directory... "
+ else
+ printn "Reading {dir}... "
end
loop
- for f in dir.files do
+ for f in list_files(dir) do
var path = dir/f
- if path.file_stat.is_dir then
+ if path.file_stat.as(not null).is_dir then
directories.push(path)
else if f.has_suffix(".xml") and f != "index.xml" then
- print "Processing {path}..."
reader.read(path)
+ file_count += 1
end
end
if directories.length <= 0 then break
dir = directories.pop
end
+ model.add_global_modules
+ print "Done."
+ if file_count < 2 then
+ print "{file_count} file read."
+ else
+ print "{file_count} files read."
+ end
+ end
+
+ # List files in a directory.
+ #
+ # This method may be redefined to force the order in which the files
+ # are read by `load_project`.
+ protected fun list_files(dir: String): Collection[String] do
+ return dir.files
+ end
+
+ # Check the project’s name.
+ private fun check_name(name: String) do
+ assert name_valid: not name.chars.first.is_upper else
+ sys.stderr.write("{sys.program_name}: The project’s name must not" +
+ " begin with an upper case letter. Got `{name}`.\n")
+ end
+ assert name_unused: not store.has_node_label(name) else
+ sys.stderr.write("{sys.program_name}: The label `{name}` is already" +
+ " used in the specified graph.\n")
+ end
end
# Save the graph.
fun save do
+ sys.stdout.write "Linking nodes...{term_save_cursor} "
model.put_edges
+ print "{term_rewind} Done."
var nodes = model.all_nodes
- print("Saving {nodes.length} nodes...")
- push_all(nodes)
+ sys.stdout.write "Saving {nodes.length} nodes..."
+ store.save_all(nodes)
var edges = model.all_edges
- print("Saving {edges.length} edges...")
- push_all(edges)
+ sys.stdout.write "Saving {edges.length} edges..."
+ store.save_all(edges)
end
+end
- # Save `neo_entities` in the database using batch mode.
- private fun push_all(neo_entities: Collection[NeoEntity]) do
- var batch = new NeoBatch(client)
- var len = neo_entities.length
- var sum = 0
- var i = 1
-
- for nentity in neo_entities do
- batch.save_entity(nentity)
- if i == batch_max_size then
- do_batch(batch)
- sum += batch_max_size
- print("\t{sum * 100 / len}% done.")
- batch = new NeoBatch(client)
- i = 1
- else
- i += 1
- end
- end
- do_batch(batch)
+# The main class.
+class NeoDoxygenCommand
+
+ # Invalid arguments
+ var e_usage = 64
+
+ # Available options for `--src-lang`.
+ var sources = new HashMap[String, SourceLanguage]
+
+ # The synopsis.
+ var synopsis: String = "[--dest <url>] [--src-lang <lang>]\n" +
+ " [--] <project_name> <doxml_dir>"
+
+ # The synopsis for the help page.
+ var help_synopsis = "[-h|--help]"
+
+ # The default destination.
+ var default_dest = "http://localhost:7474"
+
+ # Processes the options.
+ var option_context = new OptionContext
+
+ # The `--src-lang` option.
+ var opt_src_lang: OptionEnum is noinit
+
+ # The `--dest` option.
+ var opt_dest: OptionString is noinit
+
+ # The `-h|--help` option.
+ var opt_help: OptionBool is noinit
+
+ init do
+ sources["any"] = new DefaultSource
+ sources["java"] = new JavaSource
+ sources["python"] = new PythonSource
+
+ var prefix = new OptionText("""
+{{{"NAME".bold}}}
+ {{{sys.program_name}}} — Doxygen XML to Neo4j.
+
+{{{"SYNOPSIS".bold}}}
+ {{{sys.program_name}}} {{{synopsis}}}
+ {{{sys.program_name}}} {{{help_synopsis}}}
+
+{{{"DESCRIPTION".bold}}}
+ Convert a Doxygen XML output into a model in Neo4j that is readable by the
+ `nx` tool.
+
+{{{"ARGUMENTS".bold}}}
+ <project_name> The internal name of the project. Must the same name as the
+ one specified to the `nx` tool. Must not begin by an upper
+ case letter.
+
+ <doxml_dir> The directory where the XML documents generated by Doxygen are
+ located.
+
+{{{"OPTIONS".bold}}}
+""")
+ option_context.add_option(prefix)
+
+ opt_dest = new OptionString("The URL of the destination graph. `{default_dest}` by default.",
+ "--dest")
+ opt_dest.default_value = default_dest
+ option_context.add_option(opt_dest)
+
+ opt_help = new OptionBool("Show the help (this page).",
+ "-h", "--help")
+ option_context.add_option(opt_help)
+
+ var keys = new Array[String].from(sources.keys)
+ opt_src_lang = new OptionEnum(keys,
+ "The programming language to assume when processing chunks in the declarations left as-is by Doxygen. Use `any` (the default) to disable any language-specific processing.",
+ keys.index_of("any"), "--src-lang")
+ option_context.add_option(opt_src_lang)
end
- # Execute `batch` and check for errors.
- #
- # Abort if `batch.execute` returns errors.
- private fun do_batch(batch: NeoBatch) do
- var errors = batch.execute
+ # Start the application.
+ fun main: Int do
+ if args.is_empty then
+ show_help
+ return e_usage
+ end
+ option_context.parse(args)
+
+ var errors = option_context.errors
+ var rest = option_context.rest
+
+ if errors.is_empty and not opt_help.value and rest.length != 2 then
+ errors.add "Unexpected number of additional arguments. Expecting 2; got {rest.length}."
+ end
if not errors.is_empty then
- for e in errors do sys.stderr.write("{sys.program_name}: {e}\n")
- exit(1)
+ for e in errors do print_error(e)
+ show_usage
+ return e_usage
+ end
+ if opt_help.value then
+ show_help
+ return 0
end
+
+ var source = sources[opt_src_lang.value_name]
+ var dest = opt_dest.value
+ var project_name = rest[0]
+ var dir = rest[1]
+ var neo = new NeoDoxygenJob(create_store(dest or else default_dest))
+
+ neo.load_project(project_name, dir, source)
+ neo.save
+ return 0
end
-end
-if args.length != 2 and args.length != 3 then
- stderr.write("Usage: {sys.program_name} project_name xml_output_dir [neo4j_url]\n")
- exit(1)
+ # Create an instance of `GraphStore` for the specified destination.
+ protected fun create_store(dest: String): GraphStore do
+ return new Neo4jStore(new Neo4jClient(dest))
+ end
+
+ # Show the help.
+ fun show_help do
+ option_context.usage
+ end
+
+ # Show the usage.
+ fun show_usage do
+ sys.stderr.write "Usage: {sys.program_name} {synopsis}\n"
+ sys.stderr.write "For details, run `{sys.program_name} --help`.\n"
+ end
+
+ # Print an error.
+ fun print_error(e: String) do
+ sys.stderr.write "{sys.program_name}: {e}\n"
+ end
end
-var url = "http://localhost:7474"
-if args.length >= 3 then
- url = args[2]
+
+# Add handling of multi-line descriptions.
+#
+# Note: The algorithm is naive and do not handle internationalisation,
+# multi-byte characters and control characters.
+redef class Option
+
+ redef fun pretty(off) do
+ var s = super
+
+ if s.length > 80 and off < 80 then
+ var column_length = 80 - off
+ var left = 0
+ var right = 80
+ var buf = new FlatBuffer
+ var prefix = "\n{" " * off}"
+
+ loop
+ while right > left and s.chars[right] != ' ' do
+ right -= 1
+ end
+ if left == right then
+ buf.append s.substring(left, column_length)
+ right += column_length
+ else
+ buf.append s.substring(left, right - left)
+ right += 1
+ end
+ buf.append prefix
+ left = right
+ right += column_length
+ if right >= s.length then break
+ end
+ buf.append s.substring_from(left)
+ buf.append "\n"
+ return buf.to_s
+ else
+ return "{s}\n"
+ end
+ end
end
-var neo = new NeoDoxygen(new Neo4jClient(url))
-neo.put_project(args[0], args[1])
-neo.save
+exit((new NeoDoxygenCommand).main)