neo_doxygen: Do not manually flush the output.
[nit.git] / contrib / neo_doxygen / src / neo_doxygen.nit
index 7738a4a..46c6be2 100644 (file)
 # 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.
        #
@@ -52,85 +45,247 @@ class NeoDoxygen
        #
        # * `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
                                        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
+
+               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 chunk 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.get_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 and
+# escape sequences.
+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)