001    package com.sptci.echo2;
002    
003    import java.io.IOException;
004    
005    import java.lang.reflect.Field;
006    
007    import java.util.ArrayList;
008    import java.util.Date;
009    import java.util.Formatter;
010    import java.util.List;
011    import java.util.Map;
012    import java.util.TreeMap;
013    
014    import java.text.SimpleDateFormat;
015    
016    import org.rakeshv.utils.StringUtilities;
017    
018    import com.sptci.CodeGenerator;
019    import com.sptci.ReflectionUtility;
020    
021    /**
022     * A JavaBean generator.  Uses reflection to create a JavaBean that
023     * maps to the UI component fields in the specified class.
024     *
025     * <p>The following shows the invocation semantics for this class:</p>
026     * <pre>
027     *  java -classpath &lt;path to classes&gt;:sptecho.jar:&lt;path to Echo2_App.jar&gt; \
028     *    com.sptci.echo2.ModelGenerator \
029     *    &lt;fully qualified name of UI class&gt;
030     * </pre>
031     *
032     * <p>Copyright 2006 Sans Pareil Technologies, Inc.</p>
033     * @author Rakesh Vidyadharan 2006-02-07
034     * @version $Id: ModelGenerator.java,v 1.3 2006/02/15 00:48:26 rakesh Exp $
035     */
036    public class ModelGenerator extends CodeGenerator
037    {
038      /**
039       * The class for which the JavaBean is to be generated.
040       */
041      protected Class source;
042    
043      /**
044       * A <code>List</code> of import statements for the generated class.
045       */
046      protected List<String> imports;
047    
048      /**
049       * A <code>Map</code> of <code>fields</code> in {@link #source}.
050       */
051      protected Map<String, Field> uiFields;
052    
053      /**
054       * Instantiates a new instance of {@link #source} from the specified
055       * <code>name</code>.  Initialises code geneartor containers.
056       *
057       * @param name The fully qualified name of the class.  Example:
058       *   <code>com.sptci.demo.InputForm</code>.
059       * @throws ClassNotFoundException If the class corresponding to the
060       *   <code>name</code> cannot be found in the classpath.
061       */
062      public ModelGenerator( String name ) throws ClassNotFoundException
063      {
064        super();
065        source = Class.forName( name );
066    
067        this.name = source.getName().substring( 
068            source.getName().lastIndexOf( "." ) + 1 ) + "Model";
069        imports = new ArrayList<String>();
070        imports.add( "java.beans.PropertyChangeListener" );
071        imports.add( "java.beans.PropertyChangeSupport" );
072        imports.add( "java.io.Serializable" );
073        imports.add( "java.util.Formatter" );
074        uiFields = new TreeMap<String, Field>();
075      }
076    
077      /**
078       * Load the class specified, and generate a JavaBean representing its
079       * UI components.
080       *
081       * @see #processComponent
082       * @see #generateSource
083       * @throws Exception If errors are encountered while writing to 
084       *   the source file.
085       */
086      @Override
087      public void generate() throws Exception
088      {
089        for ( Map.Entry<String, Field> entry : 
090            ReflectionUtility.fetchFields( source ).entrySet() )
091        {
092          Field field = entry.getValue();
093          try
094          {
095            source.asSubclass( nextapp.echo2.app.Component.class );
096            uiFields.put( field.getName(), field );
097            processComponent( field );
098          }
099          catch ( ClassCastException ccex ) {}
100        }
101    
102        generateSource();
103      }
104    
105      /**
106       * Generate a <code>Field</code> for the JavaBean class that will be
107       * generated from the specified Echo2 UI component
108       *
109       * @param field The <code>Field</code> that represents the
110       *   Echo2 component.
111       */
112      protected void processComponent( Field field )
113      {
114        try
115        {
116          field.getType().asSubclass( 
117              nextapp.echo2.app.text.TextComponent.class );
118          fields.put( field.getName(), "String" );
119        }
120        catch ( ClassCastException ccex ) {}
121    
122        try
123        {
124          field.getType().asSubclass( 
125              java.util.Map.class );
126    
127          if ( ! imports.contains( "java.util.Map" ) )
128          {
129            imports.add( "java.util.Map" );
130            imports.add( "java.util.TreeMap" );
131          }
132    
133          fields.put( field.getName(), "Map<String, Boolean>" );
134        }
135        catch ( ClassCastException ccex ) {}
136    
137        try
138        {
139          field.getType().asSubclass( 
140              nextapp.echo2.app.list.AbstractListComponent.class );
141    
142          if ( ! imports.contains( "java.util.ArrayList" ) )
143          {
144            imports.add( "java.util.ArrayList" );
145            imports.add( "java.util.List" );
146            imports.add( "com.sptci.echo2.ListItem" );
147          }
148    
149          fields.put( field.getName(), "List<ListItem>" );
150        }
151        catch ( ClassCastException ccex ) {}
152      }
153    
154      /**
155       * Generate the source code for the JavaBean.
156       *
157       * @throws IOException If errors are encountered while writing the
158       *   generated source file.
159       */
160      protected void generateSource() throws IOException
161      {
162        StringBuilder builder = new StringBuilder( 4096 );
163        Formatter formatter = new Formatter( builder );
164    
165        formatter.format( "package %s;%n%n", source.getPackage().getName() );
166    
167        for ( String name : imports )
168        {
169          formatter.format( "import %s;%n", name );
170        }
171        formatter.format( "%n" );
172    
173        formatter.format( "/**%n" );
174        formatter.format( 
175            " * A JavaBean that represents the data represented by %n" );
176        formatter.format( " * {@link %s}.%n", source.getName() );
177        formatter.format( " *%n" );
178        formatter.format( " * @author %s %s%n", getClass().getName(),
179            ( new SimpleDateFormat( "yyyy-MM-dd HH:mm:ssZ" ) ).format( new Date() ) );
180        formatter.format( " * @version $Id: ModelGenerator.java,v 1.3 2006/02/15 00:48:26 rakesh Exp $%n" );
181        formatter.format( " */%n" );
182        formatter.format( "public class %s%n", name );
183        formatter.format( "  implements Cloneable, Comparable<%s>, Serializable%n",
184            name );
185        formatter.format( "{%n" );
186    
187        builder.append( generateFields() );
188        builder.append( generateConstructors() );
189        builder.append( generateMethods() );
190    
191        formatter.format( "}%n" );
192    
193        StringUtilities.toFile( builder.toString(), name + ".java" ); 
194      }
195    
196      /**
197       * Generate the fields for the JavaBean. Over-ridden to add code
198       * specific to the JavaBean.
199       *
200       * @see #generatePropertyChangeFields
201       * @return String The field declarations for the JavaBean.
202       */
203      protected String generateFields()
204      {
205        StringBuilder builder = new StringBuilder( 4096 );
206        Formatter formatter = new Formatter( builder );
207    
208        builder.append( super.generateFields() );
209    
210        for ( Map.Entry<String, String> entry : fields.entrySet() )
211        {
212          formatter.format( "  /**%n" );
213          formatter.format( 
214              "   * Field that represents the %s field of type%n",
215              entry.getKey() );
216          formatter.format( "   * %s%n", 
217              uiFields.get( entry.getKey() ).getName() );
218          formatter.format( "   */%n" );
219          formatter.format( "  private %s %s;%n%n",
220              entry.getValue(), entry.getKey() );
221        }
222    
223        builder.append( generatePropertyChangeFields() );
224    
225        return builder.toString();
226      }
227    
228      /**
229       * Generate the fields necessary to support PropertyChangeListener
230       * ability for the bean.
231       *
232       * @return String The field declarations necessary for property
233       *   change support
234       */
235      protected String generatePropertyChangeFields()
236      {
237        StringBuilder builder = new StringBuilder( 512 );
238        Formatter formatter = new Formatter( builder );
239    
240        formatter.format( "  /**%n" );
241        formatter.format( "   * The property change event dispatcher.  This object%n" );
242        formatter.format( "   * is lazily loaded.%n" );
243        formatter.format( "   */%n" );
244        formatter.format( "  private transient PropertyChangeSupport propertyChangeSupport;%n%n" );
245    
246        return builder.toString();
247      }
248    
249      /**
250       * Generate the constructors for the JavaBean.
251       *
252       * @see #generateDefaultConstructor
253       * @see #generateDesignatedConstructor
254       */
255      protected String generateConstructors()
256      {
257        StringBuilder builder = new StringBuilder( 4096 );
258    
259        builder.append( generateDefaultConstructor() );
260        builder.append( generateDesignatedConstructor() );
261    
262        return builder.toString();
263      }
264    
265      /**
266       * Generate the default constructor for the JavaBean
267       */
268      protected String generateDefaultConstructor()
269      {
270        StringBuilder builder = new StringBuilder( 1024 );
271        Formatter formatter = new Formatter( builder );
272    
273        formatter.format( "  /**%n" );
274        formatter.format( "   * Default constructor.  Uses the designated constructor.%n" );
275        formatter.format( "   */%n" );
276        formatter.format( "  public %s()%n", name );
277        formatter.format( "  {%n" );
278        formatter.format( "    this( " );
279    
280        int count = 0;
281        for ( Map.Entry<String, String> entry : fields.entrySet() )
282        {
283          if ( count != 0 )
284          {
285            formatter.format( ",%n    " );
286          }
287    
288          if ( entry.getValue().equals( "Map<String, Boolean>" ) )
289          {
290            formatter.format( " new Tree%s()", entry.getValue() );
291          }
292          else if ( entry.getValue().equals( "List<ListItem>" ) )
293          {
294            formatter.format( " new Array%s()", entry.getValue() );
295          }
296          else
297          {
298            formatter.format( " null" );
299          }
300    
301          ++count;
302        }
303        formatter.format( " );%n" );
304        formatter.format( "  }%n%n" );
305    
306        return builder.toString();
307      }
308    
309      /**
310       * Generate the designated constructor for the JavaBean
311       */
312      protected String generateDesignatedConstructor()
313      {
314        StringBuilder builder = new StringBuilder( 1024 );
315        Formatter formatter = new Formatter( builder );
316    
317        formatter.format( "  /**%n" );
318        formatter.format( "   * Designated constructor.  Initialises the%n" );
319        formatter.format( "   * fields with the specified values.%n" );
320        formatter.format( "   */%n" );
321        formatter.format( "  public %s( ", name );
322    
323        int count = 0;
324        for ( Map.Entry<String, String> entry : fields.entrySet() )
325        {
326          if ( count != 0 )
327          {
328            formatter.format( ",%n    " );
329          }
330    
331          formatter.format( "%s %s", entry.getValue(), entry.getKey() );
332          ++count;
333        }
334        formatter.format( " )%n" );
335    
336        formatter.format( "  {%n" );
337        for ( Map.Entry<String, String> entry : fields.entrySet() )
338        {
339          formatter.format( "    set%s%s( %s );%n",
340              entry.getKey().substring( 0, 1 ).toUpperCase(),
341              entry.getKey().substring( 1 ),
342              entry.getKey() );
343        }
344        formatter.format( "  }%n%n" );
345    
346        return builder.toString();
347      }
348    
349      /**
350       * Generate the methods for the JavaBean.
351       *
352       * @see #generateToStringMethod
353       * @see #generateEqualsMethod
354       * @see #generateHashCodeMethod
355       * @see #generateCompareToMethod
356       * @see #generateCloneMethod
357       * @see #generatePropertyChangeMethods
358       * @see #generateBeanMethods
359       * @return String The source code for the methods to be included in
360       *  the JavaBean.
361       */
362      protected String generateMethods()
363      {
364        StringBuilder builder = new StringBuilder( 4096 );
365        builder.append( generateToStringMethod() );
366        builder.append( generateEqualsMethod() );
367        builder.append( generateHashCodeMethod() );
368        builder.append( generateCompareToMethod() );
369        builder.append( generateCloneMethod() );
370        builder.append( generatePropertyChangeMethods() );
371        builder.append( generateBeanMethods() );
372    
373        return builder.toString();
374      }
375    
376      /**
377       * Generate the methods to support property change events.
378       *
379       * @return String The source code for property change event
380       *   handling.
381       */
382      protected String generatePropertyChangeMethods()
383      {
384        StringBuilder builder = new StringBuilder( 1024 );
385        Formatter formatter = new Formatter( builder );
386    
387        formatter.format( "  /**%n" );
388        formatter.format( "   * Adds a property change listener to this <code>bean</code>.%n" );
389        formatter.format( "   * Delegates handling to {@link #propertyChangeSupport}%n" );
390        formatter.format( "   *%n" );
391        formatter.format( "   * @param listener The listener to add.%n" );
392        formatter.format( "   */%n" );
393        formatter.format( "  public void addPropertyChangeListener( PropertyChangeListener listener )%n" );
394        formatter.format( "  {%n" );
395        formatter.format( "    if ( propertyChangeSupport == null )%n" );
396        formatter.format( "    {%n" );
397        formatter.format( "      propertyChangeSupport = new PropertyChangeSupport( this );%n" );
398        formatter.format( "    }%n%n" );
399        formatter.format( "    propertyChangeSupport.addPropertyChangeListener( listener );%n" );
400        formatter.format( "  }%n%n" );
401    
402        formatter.format( "  /**%n" );
403        formatter.format( "   * Removes a property change listener from this <code>bean</code>.%n" );
404        formatter.format( "   * Delegates handling to {@link #propertyChangeSupport}%n" );
405        formatter.format( "   *%n" );
406        formatter.format( "   * @param listener The listener to be removed.%n" );
407        formatter.format( "   */%n" );
408        formatter.format( "  public void removePropertyChangeListener(%n" );
409        formatter.format( "      PropertyChangeListener listener )%n" );
410        formatter.format( "  {%n" );
411        formatter.format( "    if ( propertyChangeSupport != null )%n" );
412        formatter.format( "    {%n" );
413        formatter.format( "      propertyChangeSupport.removePropertyChangeListener( listener );%n" );
414        formatter.format( "    }%n" );
415        formatter.format( "  }%n%n" );
416    
417        formatter.format( "  /**%n" );
418        formatter.format( "   * Reports a bound property change to the registerd%n" );
419        formatter.format( "   * <code>PropertyChangeListener</code>s.%n" );
420        formatter.format( "   * Delegates handling to {@link #propertyChangeSupport}%n" );
421        formatter.format( "   *%n" );
422        formatter.format( "   * @param propertyName The name of the changed property.%n" );
423        formatter.format( "   * @param oldValue The previous value of the property.%n" );
424        formatter.format( "   * @param newValue The present value of the property.%n" );
425        formatter.format( "   */%n" );
426        formatter.format( "  protected void firePropertyChange( String propertyName,%n" );
427        formatter.format( "      Object oldValue, Object newValue )%n" );
428        formatter.format( "  {%n" );
429        formatter.format( "    if ( propertyChangeSupport != null )%n" );
430        formatter.format( "    {%n" );
431        formatter.format( "      propertyChangeSupport.firePropertyChange(%n" );
432        formatter.format( "          propertyName, oldValue, newValue );%n" );
433        formatter.format( "    }%n" );
434        formatter.format( "  }%n%n" );
435    
436        return builder.toString();
437      }
438    
439      /**
440       * Over-ridden to fire <code>PropertyChangeEvent</code>s for 
441       * mutator methods.
442       *
443       * @return String The default accessor and mutator methods for
444       *   the {@link #fields}.
445       */
446      @Override
447      protected String generateBeanMethods()
448      {
449        StringBuilder methods = 
450          new StringBuilder( super.generateBeanMethods() );
451        String mutator = "public final void set";
452        int index = 0;
453        while ( ( index = methods.indexOf( mutator, index + 1 ) ) != -1 )
454        {
455          String propertyName = methods.substring( index + mutator.length(),
456              methods.indexOf( "(", index ) ).trim();
457          propertyName = propertyName.substring( 0, 1 ).toLowerCase() +
458              propertyName.substring( 1 );
459    
460          StringBuilder builder = new StringBuilder( 32 );
461          Formatter formatter = new Formatter( builder );
462          formatter.format( "    firePropertyChange( \"%s\", oldValue, %s );%n",
463              propertyName, propertyName );
464    
465          int end = methods.indexOf( "  }", index );
466          methods.insert( end, builder.toString() );
467        }
468    
469        return methods.toString();
470      }
471    
472      /**
473       * Main method.
474       */
475      public static void main( String[] args )
476      {
477        if ( args.length != 1 )
478        {
479          System.err.println( 
480              "Usage: java -classpath <classpath> com.sptci.echo2.ModelGenerator <UI class>" );
481          System.exit( 2 );
482        }
483        else
484        {
485          try
486          {
487            ModelGenerator generator = new ModelGenerator( args[0] );
488            generator.generate();
489          }
490          catch ( Throwable t )
491          {
492            t.printStackTrace();
493            System.exit( 1 );
494          }
495        }
496    
497        System.exit( 0 );
498      }
499    }