The text document below outlines some proposed changes
to the Ant "data model" to make it more friendly as an
API. Although it is somewhat long, I'd really
appreciate any feedback you can provide.

__________________________________________________
Do You Yahoo!?
Thousands of Stores.  Millions of Products.  All in one Place.
http://shopping.yahoo.com/
The Antidote work has started to uncover areas where some of the
foundation Ant classes could be better defined and/or encapsulated. I
have a set of first round recommendations below for modifying
specifically Main.java, Project.java, and Target.java. The focus is
organizational; none change the basic behaviors of these classes, just
the partitioning and accessibility of functionality.

Some of the proposed partitioning may not be /immediately/ identified
as necessary (other than for clean-up purposes) or having any value
added properties. But in most cases the changes I propose originate in
thoughts on where I could leverage the repartitioned classes in Antidote.

Also, don't expect people to jump in and make these changes. The
changes that everyone agrees to I'm happy to jump in and make
myself. Someone propose a task allocation approach.

Enough preliminaries, here are my thoughts:

A) Main.java has quite a lot of stuff in it, and I think the project
   would be better served if it were more of a component assembly
   point than a catch-all function container. It basically executes
   the following tasks:

   1) Parse arguments and set state of runtime options
   2) Parse properties (-D entries).
   3) Build target list.
   4) Find build file.
   5) Create a Project instance.
   6) Add build listeners.
   7) Fire "buildStarted" event.
   8) Initialize the Project class.
   9) Populate Project properties.
   10) Initialize XML parser.
   11) Parse XML build file (via ProjectHelper).
   12) Execute specified targets.
   13) Fire "buildFinished" event.

   It also serves the following roles:

   1) A logging facility (printMessage()).
   2) File path decomposing (getParentFile()).
   3) A build file location facility (findBuildFile()).
   4) Command line usage reporting.
   5) Version management reporting facility.
   6) Target sorting facility (findTargetPosition()).
   7) Target listing facility (printTargets()).

B) The Project.java file also has a lot of fungal growth :-P. It's
   "init()" method (which is is separate from the constructor, I
   assume for BuildEvent reasons) executes the following tasks:

   1) Figures out what version of Java it is running under.
   2) Loads the default properties from default.properties.
   3) Loads type definitions.
   4) Adds the system properties to the project properties.

   It also serves the following API roles.

   1) Enumeration value container for the logging level (MSG_ERR, MSG_WARN,
   etc).
   2) Target sorting and searching functions (topoSort(), tsort()).
   3) Java version management.
   4) Property management (properties, userProperties).
   5) Token filtering and replacement code (replace(); the "@foo@" stuff).
   6) BuildListener management (add, remove, fire).
   7) Logger functionality (log(xxx) methods).
   8) Task manager (createTask()).
   9) Data type manager (createDataType(), addDataTypeDefinition(),
   toBoolean()). 
   10) Target manager (addTarget(), addOrReplaceTarget()).
   11) Path parsing/translating (translatePath(), resolveFile());
   12) File copying support(copyFile(xxx) methods (6)).
   13) Build execution (runTarget(), executeTarget(), executeTargets())

   Quite a lot of responsibility for one class.

C) Here is /a/ proposed refactoring:
   1) Create a class called "Args" that is responsible for:
      a) Parsing the command line arguments,
      b) Reporting argument parsing errors and generation of "usage"
      text
      c) Population of "Args" data members which contains the
      results of parsing the arguments.

      The "Args" class would look something like this:
          class Args {
                Args(String[] args) throws InvalidArgumentException;
                File getBuildFile();
                String[] getTargetNames();
                Properties getProperties();
                BuildListener[] getBuildListeners();
                PrintWriter getStdOut();
                PrintWriter getStdErr();
                boolean isOutputUnadorned(); // Emacs mode...
                LogLevelEnum getLogLevel(); // See LogLevelEnum in Antidote
                String getUsage();
          }               

   2) Create a "Logger" class. It would be initialized with the return
      values of Args.getStdOut(), Args.getStdErr(),
      Args.getLogLevel(), and probably some other log listener
      method. Initially this can just be a simple class, but provides
      the proper abstraction for additional features down the
      road. The "Logger" class would be passed to the Project class
      for it's logging facility.

   3) The logging level "static final int" variables (MSG_WARN,
      MSG_INFO, etc.) belong either in BuildEvent, the Proposed
      "Logger" class, or in a separate enumeration class. My
      preference is the latter, which is already implemented in
      Antidote as "LogLevelEnum" and can be moved to the ant package
      if that approach is used.

   4) Create a "AntProperties" class, which takes care of loading an
      overriding properties based on the default.properties, command
      line properties and .ant.properties data sets.

   5) Create a "DataTypeManager" that is responsible for loading and
      initializing the data types based on the property sets.

   6) Better encapsulation of the event handling. In Antidote there is
      an "EventBus" whose sole role is to handle listener registration
      and event dispatching. I don't think going this far is necessary
      for Ant, but there should be a *single* place for registering
      BuildListener instances and firing events.

      I think the Project class is still a good place to have the
      add/remove listener methods (since Target and Task have access
      to the current Project), but access to the event firing code
      needs to be opened up, and having 10 different "fireXXX()"
      methods is a little unweildy. In Antidote I handle this by
      having an enumeration class called "BuildEventType" which
      represents the type of BuildEvent (BUILD_STARTED,
      MESSAGE_LOGGED, etc.), and knows how to dispatch an event to the
      proper BuildListener method when needed. This allows me to have
      a single "fireEvent(BuildEvent,BuildEventType)" method, which
      delegates the firing to the BuildEventType class.

   7) Relocation of XML parser initialization to the
      ProjectHelper. Although I have yet to really dive into the the
      ProjectHelper class, my impression is that it is really the
      container of the "SAX" listener and the binding between XML
      elements and specific classes; the XML factory of sorts. Since
      the XML binding appears to be hidden behind here, I recommend
      the loading of the SAXParser be moved from Main.java to a static
      initialization block in ProjectHelper.

   8) The need for separating the initialization code in the Project
      class into an "init()" method instead of in the constructor can
      be eliminated if the Project class is passed the set of
      listeners (from the Args class) into it's constructor. (Am I
      correct here?)

   9) The "executeTarget{s}(), runTarget()" code in Project.java can
      be cleaned up some, in addition to moving the
      "fireBuild{Started|Finished}()" invocation from Main.java into
      the "executeTarget()" method. This is an area where Antidote
      is currently having to copy code from Main.java to invoke a build.

   10) Either colocate the file based utility methods (fileCopy(),
       getParentFile()), or establish an approach to behavioral
       encapsulation, like a Command type of pattern, and add more
       application specific types, like a BuildFile class. I'm often a
       proponent of the latter, where you subclass a type like
       java.io.File as "BuildFile", and define the constructors in an
       application specific manner (e.g. took an Args object in the
       constructor), and throw Exceptions when they aren't initialized
       properly (build file didn't exist in specified location). Then
       you can put methods like "getParentFile()" in there.

   11) Create a "TargetCollection" class that manages a set of
       targets, and knows how to sort them in "target" specific
       ways. I think an instance of this class should be owned by the
       Project class (in leiu of a Hashtable), but that the storing
       and sorting semantics be encapsulated. For example, in Antidote
       I need a "flattened" set of dependencies (not just the
       immediate dependences, but complete dependency graph from a
       given node. Right now I can't use any of the related methods
       stuffed in the Project class because they're private, nor
       should "topological sorting" be considered a responsibility of
       the Project class, which I would define as more of a data class
       than an algorithm class. A "TargetCollection" would be the
       perfect place to put this sorting functionality, as well as
       other target management features.

   12) There has been a discussion lately about a more general
       token replacement facility. If this comes to fruition,
       hopefully the code to do this will be moved out of the Project class.

   13) Although this is another area where I haven't studied the APIs
       in depth, the management of Tasks and "DataTypes" should be
       relegated to appropriate "Manager" or "Factory"
       classes. Although a Project should still contain zero or more
       Targets, which should contain zero or more tasks Task, which
       use zero or more DataTypes, etc., the dynamic loading and
       configuring of Tasks and DataTypes should be partitioned out.

   14) The API needs to be tuned up for reentrancy and thread
       safty. When run from the command line this is not an issue, but
       Antidote has to launch the build in a separate thread, has to
       implement its own locking semantics around the Project
       class. Additionally there is no support for stopping a build
       once it has started (other than suspending/stopping the build
       thread, which is a big "no no"). The capability for setting an
       "interrupt" flag in the task execution code, and even support
       for killing off forked processes is needed. Ctrl-C doesn't
       quite do it here :-].

   15-99) For another email :-) This is getting to be too much...

   100) Javadoc, javadoc, javadoc. I think Ant has reach a maturity
        level where we should start thinking of it as a full-fledged
        API in addition to a stand-alone tool. More detailed javadoc
        is needed to describe the behavior and class semantics so that
        "Blackbox" reuse can occur rather than mostly "Whitebox" or
        "cut-n-paste" reuse. The process of adding more javadoc will
        also help in the refactoring process as it forces one to think
        about roles and behaviors in a concise manner.

Sorry for the volumes of text, but I think my hitting Ant as an API
rather than a command line tool has challenged it in new ways. If
there is a single message in all this it is to make Project, Target,
and Task more data oriented, and encapsulate the various services that
are currently stuffed in them to classes with clearly defined roles
fulfilling these services. Without diverging too much, I think
modelling the Project, Target, and Task classes more on a DOM patter
will serve the project well in the long term.

Simeon

Reply via email to