View Javadoc

1   package org.apache.maven.changelog;
2   
3   /* ====================================================================
4    *   Licensed to the Apache Software Foundation (ASF) under one or more
5    *   contributor license agreements.  See the NOTICE file distributed with
6    *   this work for additional information regarding copyright ownership.
7    *   The ASF licenses this file to You under the Apache License, Version 2.0
8    *   (the "License"); you may not use this file except in compliance with
9    *   the License.  You may obtain a copy of the License at
10   *
11   *       http://www.apache.org/licenses/LICENSE-2.0
12   *
13   *   Unless required by applicable law or agreed to in writing, software
14   *   distributed under the License is distributed on an "AS IS" BASIS,
15   *   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
16   *   See the License for the specific language governing permissions and
17   *   limitations under the License.
18   * ====================================================================
19   */
20  
21  // java imports
22  import java.io.File;
23  import java.io.FileNotFoundException;
24  import java.io.FileOutputStream;
25  import java.io.IOException;
26  import java.io.OutputStreamWriter;
27  import java.io.PrintWriter;
28  import java.io.UnsupportedEncodingException;
29  
30  import java.util.ArrayList;
31  import java.util.Collection;
32  import java.util.Collections;
33  import java.util.HashMap;
34  import java.util.Iterator;
35  import java.util.List;
36  import java.util.Map;
37  import java.util.Properties;
38  import java.util.StringTokenizer;
39  
40  import org.apache.commons.logging.Log;
41  import org.apache.commons.logging.LogFactory;
42  import org.apache.maven.project.Developer;
43  import org.apache.maven.util.RepositoryUtils;
44  
45  /**
46   * Change log task. It uses a ChangeLogGenerator and ChangeLogParser to create
47   * a Collection of ChangeLogEntry objects, which are used to produce an XML
48   * output that represents the list of changes.
49   *
50   *
51   * @author <a href="mailto:glenn@somanetworks.com">Glenn McAllister</a>
52   * @author <a href="mailto:jeff.martin@synamic.co.uk">Jeff Martin</a>
53   * @author <a href="mailto:jason@zenplex.com">Jason van Zyl</a>
54   * @author <a href="mailto:dion@multitask.com.au">dIon Gillard</a>
55   * @author <a href="mailto:bodewig@apache.org">Stefan Bodewig</a>
56   * @author <a href="mailto:peter@apache.org">Peter Donald</a>
57   * @version $Id: ChangeLog.java 532339 2007-04-25 12:28:56Z ltheussl $
58   */
59  public class ChangeLog
60  {
61      private static final Map FACTORIES = new HashMap();
62  
63      /** Log */
64      private static final Log LOG = LogFactory.getLog( ChangeLog.class );
65  
66      static
67      {
68          FACTORIES.put( "cvs", "org.apache.maven.cvslib.CvsChangeLogFactory" );
69          FACTORIES.put( "svn", "org.apache.maven.svnlib.SvnChangeLogFactory" );
70          FACTORIES.put( "clearcase",
71              "org.apache.maven.clearcaselib.ClearcaseChangeLogFactory" );
72          FACTORIES.put( "perforce",
73              "org.apache.maven.perforcelib.PerforceChangeLogFactory" );
74          FACTORIES.put( "starteam",
75              "org.apache.maven.starteamlib.StarteamChangeLogFactory" );
76          FACTORIES.put( "vss", "org.apache.maven.vsslib.VssChangeLogFactory" );
77          FACTORIES.put( "mks", "org.apache.maven.mkslib.MksChangeLogFactory" );
78      }
79  
80      /** Used to specify whether to build the log from a range, absolute date, or tag. */
81      private String type;
82  
83      /**
84       * Used to specify the range of log entries to retrieve.
85       */
86      private String range;
87  
88      /** Used to specify the absolute date (or list of dates) to start log entries from. */
89      private String date;
90  
91      /** Used to specify the tag (or list of tags) to start log entries from. */
92      private String tag;
93  
94      /** This will contain the date/tag for the start of the current change set. */
95      private String markerStart;
96  
97      /** This will contain the date/tag for the end of the current change set. */
98      private String markerEnd;
99  
100     /**
101     * Used to specify the date format of log entries to retrieve.
102     */
103     private String dateFormat;
104 
105     /**
106     * Specifies whether to quote date argument (used by CvsChangeLogGenerator).
107     */
108     private boolean quoteDate;
109 
110     /**
111      * Input dir. Working directory for running CVS executable
112      */
113     private File base;
114 
115     /**
116      * The classname of our ChangeLogFactory, defaulting to Maven's built in
117      * CVS factory.
118      */
119     private String clFactoryClass = null;
120 
121     /** the connection string used to access the SCM */
122     private String connection;
123 
124     /** the list of developers on the project */
125     private List developers;
126 
127     /** change log sets parsed (sets of entries) */
128     private Collection sets;
129 
130     /**
131      * Output file for xml document
132      */
133     private File output;
134 
135     /** output encoding for the xml document */
136     private String outputEncoding;
137 
138     /**
139      * Comment format string used for interrogating
140      * the revision control system.
141      * Currently only used by the ClearcaseChangeLogGenerator.
142      */
143     private String commentFormat;
144 
145     /**
146      * Set the ChangeLogFactory class name.  If this isn't set, the factory
147      * defaults to Maven's build in CVS factory.
148      *
149      * @param factoryClassName the fully qualified factory class name
150      */
151     public void setFactory( String factoryClassName )
152     {
153         clFactoryClass = factoryClassName;
154     }
155 
156     /**
157      * Set the type of log to generate (range, date, or tag).
158      *
159      * @param type  one of "range", "date", or "tag".
160      */
161     public void setType( String type )
162     {
163         this.type = type;
164     }
165 
166     /**
167      * Get the type of log to generate (range, date, or tag).
168      *
169      * @return  the basis for the log.
170      */
171     public String getType()
172     {
173         if ( type == null )
174         {
175             type = "range";
176         }
177 
178         return type;
179     }
180 
181     /**
182      * Set the range of log entries to process; the interpretation of this
183      * parameter depends on the generator.
184      * This is only used if the type is "range".
185      *
186      * @param range the range of log entries.
187      */
188     public void setRange( String range )
189     {
190         this.range = range;
191     }
192 
193     /**
194      * Get the range of log entries to process; the interpretation of the range
195      * depends on the generator implementation.
196      *
197      * @return the range of log entries.
198      */
199     public String getRange()
200     {
201         return range;
202     }
203 
204     /**
205      * Set the date to start the log from.
206      * This is only used if the type is "date".
207      * The format is that given by the dateFormat property, if present. Otherwise, the format is "yyyy-MM-dd".
208      *
209      * @param date  the date to use.
210      */
211     public void setDate( String date )
212     {
213         this.date = date;
214     }
215 
216     /**
217      * Get the date to start the log from.
218      *
219      * @return  the start date.
220      */
221     public String getDate()
222     {
223         return date;
224     }
225 
226     /**
227      * Set the tag to start the log from.
228      * This is only used if the type is "tag".
229      *
230      * @param tag  the tag to use.
231      */
232     public void setTag( String tag )
233     {
234         this.tag = tag;
235     }
236 
237     /**
238      * Get the tag to start the log from.
239      *
240      * @return  the tag.
241      */
242     public String getTag()
243     {
244         return tag;
245     }
246 
247     /**
248      * Sets the marker (date or tag) for the start of the current change set.
249      * (This is only set internally, but also by test code.)
250      *
251      * @param marker  start marker to use.
252      */
253     public void setMarkerStart( String marker )
254     {
255         markerStart = marker;
256     }
257 
258     /**
259      * Returns the marker (date or tag) for the start of the current change set.
260      * Whether it's a date or tag depends on {@link #getType}.
261      *
262      * @return  the marker (date or tag) for the start of the current change set.
263      */
264     public String getMarkerStart()
265     {
266         return markerStart;
267     }
268 
269     /**
270      * Sets the marker (date or tag) for the end of the current change set.
271      * (This is only set internally, but also by test code.)
272      *
273      * @param marker  end marker to use, or <code>null</code> to specify all changes since the start.
274      */
275     public void setMarkerEnd( String marker )
276     {
277         markerEnd = marker;
278     }
279 
280     /**
281      * Returns the marker (date or tag) for the end of the current change set.
282      * Whether it's a date or tag depends on {@link #getType}.
283      *
284      * @return  the marker (date or tag) for the end of the current change set, or <code>null</code> if there is no
285      *          end (meaning the change set should show all changes from the start to the present time).
286      */
287     public String getMarkerEnd()
288     {
289         return markerEnd;
290     }
291 
292     /**
293      * Set the date format of log entries to process; the
294      * interpretation of this parameter depends on the generator.
295      *
296      * @param dateFormat the dateFormat of log entries.
297      */
298     public void setDateFormat( String dateFormat )
299     {
300         this.dateFormat = dateFormat;
301     }
302 
303     /**
304      * Get the date format of log entries to process; the
305      * interpretation of this parameter depends on the generator.
306      *
307      * @return the dateFormat of log entries.
308      */
309     public String getDateFormat()
310     {
311         return dateFormat;
312     }
313 
314     /**
315      * Set the quoteDate property.
316      * @param newQuoteDate the quoteDate property to set.
317      */
318     public void setQuoteDate( boolean newQuoteDate )
319     {
320         this.quoteDate = newQuoteDate;
321     }
322 
323     /**
324      * Get the quoteDate property.
325      *
326      * @return the quoteDate property.
327      */
328     public boolean getQuoteDate()
329     {
330         return quoteDate;
331     }
332 
333     /**
334      * Set the base directory for the change log generator.
335      * @param base the base directory
336      */
337     public void setBasedir( File base )
338     {
339         this.base = base;
340     }
341 
342     /**
343      * Get the base directory for the change log generator.
344      *
345      * @return the base directory
346      */
347     public File getBasedir()
348     {
349         return base;
350     }
351 
352     /**
353      * Set the output file for the log.
354      * @param output the output file
355      */
356     public void setOutput( File output )
357     {
358         this.output = output;
359     }
360 
361     /**
362      * Return connection string declared in project.xml
363      * @return connection string
364      */
365     public String getRepositoryConnection()
366     {
367         return connection;
368     }
369 
370     /**
371      * Change SCM connection string
372      * @param aString a string containing the project's repository
373      *      connection
374      */
375     public void setRepositoryConnection( String aString )
376     {
377         connection = aString;
378     }
379 
380     /**
381      * Execute task.
382      * @throws FileNotFoundException if the base diretory
383      * {@link ChangeLog#getBasedir} doesn't exist
384      * @throws IOException if there are problems running CVS
385      * @throws UnsupportedEncodingException if the underlying platform doesn't
386      *      support ISO-8859-1 encoding
387      */
388     public void doExecute()
389         throws FileNotFoundException, IOException, UnsupportedEncodingException
390     {
391         if ( output == null )
392         {
393             throw new NullPointerException( "output must be set" );
394         }
395 
396         generateSets();
397         replaceAuthorIdWithName();
398         createDocument();
399     }
400 
401     /**
402      * Create the change log entries.
403      * @throws IOException if there is a problem creating the change log
404      * entries.
405      */
406     private void generateSets()
407         throws IOException
408     {
409         ChangeLogFactory factory = createFactory();
410 
411         String markers = "";
412 
413         if ( getType().equalsIgnoreCase( "tag" ) )
414         {
415             markers = getTag();
416         }
417         else if ( getType().equalsIgnoreCase( "date" ) )
418         {
419             markers = getDate();
420         }
421         else
422         {
423             markers = getRange();
424         }
425 
426         try
427         {
428             StringTokenizer tokens = new StringTokenizer( markers, "," );
429 
430             sets = new ArrayList( tokens.countTokens() );
431 
432             String end = tokens.nextToken();
433 
434             do
435             {
436                 String start = end;
437 
438                 end = ( tokens.hasMoreTokens() ) ? tokens.nextToken() : null;
439                 setMarkerStart( start );
440                 setMarkerEnd( end );
441 
442                 ChangeLogParser parser = factory.createParser();
443 
444                 if ( getDateFormat() != null )
445                 {
446                     parser.setDateFormatInFile( getDateFormat() );
447                 }
448 
449                 parser.init( this );
450 
451                 ChangeLogGenerator generator = factory.createGenerator();
452 
453                 generator.init( this );
454 
455                 Collection entries;
456                 String logStart;
457                 String logEnd;
458 
459                 try
460                 {
461                     entries = generator.getEntries( parser );
462                     logStart = generator.getLogStart();
463                     logEnd = generator.getLogEnd();
464                 }
465                 catch ( IOException e )
466                 {
467                     LOG.warn( e.getLocalizedMessage(), e );
468                     throw e;
469                 }
470                 finally
471                 {
472                     generator.cleanup();
473                     parser.cleanup();
474                 }
475 
476                 if ( entries == null )
477                 {
478                     entries = Collections.EMPTY_LIST;
479                 }
480 
481                 sets.add( new ChangeLogSet( entries, logStart, logEnd ) );
482 
483                 if ( LOG.isInfoEnabled() )
484                 {
485                     LOG.info( "ChangeSet between " + logStart + " and "
486                         + logEnd + ": " + entries.size() + " entries" );
487                 }
488             }
489             while ( end != null );
490         }
491         finally
492         {
493         }
494     }
495 
496     /**
497      * Create a new instance of the ChangeLogFactory specified by the
498      * <code>clFactory</code> member.
499      *
500      * @return the new ChangeLogFactory instance
501      * @throws IOException if there is a problem creating the instance.
502      */
503     private ChangeLogFactory createFactory()
504         throws IOException
505     {
506         if ( clFactoryClass == null )
507         {
508             // Connection Format: scm:<provider><separator><provider specific connection string>
509             if ( ( connection == null ) || ( connection.length() < 5 )
510                 || !connection.startsWith( "scm:" ) )
511             {
512                 LOG.warn( "Connection does not appear valid" );
513             }
514             else
515             {
516                 int iProviderEnd = connection.indexOf( RepositoryUtils.getSCMConnectionSeparator( connection ) , 4 );
517                 if ( iProviderEnd == -1 )
518                 {
519                     // Connection = scm:<provider>
520                     iProviderEnd = connection.length();
521                 }
522 
523                 clFactoryClass = (String) FACTORIES.get( connection.substring( 4, iProviderEnd ) );
524             }
525 
526             if ( clFactoryClass == null )
527             {
528                 LOG.warn(
529                     "Could not derive factory from connection: using CVS (valid factories are: "
530                     + FACTORIES.keySet() + ")" );
531                 clFactoryClass = "org.apache.maven.cvslib.CvsChangeLogFactory";
532             }
533         }
534 
535         try
536         {
537             Class clazz = Class.forName( clFactoryClass );
538 
539             return (ChangeLogFactory) clazz.newInstance();
540         }
541         catch ( ClassNotFoundException cnfe )
542         {
543             throw new IOException( "Cannot find class " + clFactoryClass + " "
544                 + cnfe.toString() );
545         }
546         catch ( IllegalAccessException iae )
547         {
548             throw new IOException( "Cannot access class " + clFactoryClass
549                 + " " + iae.toString() );
550         }
551         catch ( InstantiationException ie )
552         {
553             throw new IOException( "Cannot instantiate class " + clFactoryClass
554                 + " " + ie.toString() );
555         }
556     }
557 
558     /**
559      * Set up list of developers mapping id to name.
560      * @task This should be a facility on the maven project itself
561      * @return a list of developers ids and names
562      */
563     private Properties getUserList()
564     {
565         Properties userList = new Properties();
566 
567         Developer developer = null;
568 
569         for ( Iterator i = getDevelopers().iterator(); i.hasNext(); )
570         {
571             developer = (Developer) i.next();
572             if (developer.getId() != null) {
573 				userList.put(developer.getId(),
574 						developer.getName() == null ? developer.getId()
575 								: developer.getName());
576 			}
577             else
578             {
579                 LOG.warn( "WARNING: Some developer entries don't have an id." );
580                 LOG.warn( "         Your changelog- and developer-activity reports will not be complete!" );
581             }
582         }
583 
584         return userList;
585     }
586 
587     /**
588      * replace all known author's id's with their maven specified names
589      */
590     private void replaceAuthorIdWithName()
591     {
592         Properties userList = getUserList();
593         ChangeLogEntry entry = null;
594 
595         for ( Iterator iSets = getChangeSets().iterator();
596             iSets.hasNext(); )
597         {
598             final ChangeLogSet set = (ChangeLogSet) iSets.next();
599 
600             for ( Iterator iEntries = set.getEntries().iterator();
601                 iEntries.hasNext(); )
602             {
603                 entry = (ChangeLogEntry) iEntries.next();
604 
605                 if ( userList.containsKey( entry.getAuthor() ) )
606                 {
607                     entry.setAuthor( userList.getProperty( entry.getAuthor() ) );
608                 }
609             }
610         }
611     }
612 
613     /**
614      * Create the XML document from the currently available details
615      * @throws FileNotFoundException when the output file previously provided
616      *      does not exist
617      * @throws UnsupportedEncodingException when the platform doesn't support
618      *      ISO-8859-1 encoding
619      */
620     private void createDocument()
621         throws FileNotFoundException, UnsupportedEncodingException
622     {
623         File dir = output.getParentFile();
624 
625         if ( dir != null )
626         {
627             dir.mkdirs();
628         }
629 
630         PrintWriter out =
631             new PrintWriter( new OutputStreamWriter( 
632                     new FileOutputStream( output ), getOutputEncoding() ) );
633 
634         out.println( toXML() );
635         out.flush();
636         out.close();
637     }
638 
639     /**
640      * @return an XML document representing this change log and it's entries
641      */
642     private String toXML()
643     {
644         StringBuffer buffer = new StringBuffer();
645 
646         buffer.append( "<?xml version=\"1.0\" encoding=\"" )
647               .append( getOutputEncoding() ).append( "\" ?>\n" ).append( "<changelog>\n" );
648 
649         for ( Iterator i = getChangeSets().iterator(); i.hasNext(); )
650         {
651             buffer.append( ( (ChangeLogSet) i.next() ).toXML() );
652         }
653 
654         buffer.append( "</changelog>\n" );
655 
656         return buffer.toString();
657     }
658 
659     /**
660      * Gets the collection of change sets.
661      *
662      * @return collection of {@link ChangeLogSet} objects.
663      */
664     public Collection getChangeSets()
665     {
666         if ( sets == null )
667         {
668             sets = Collections.EMPTY_LIST;
669         }
670 
671         return sets;
672     }
673 
674     /**
675      * Sets the collection of change sets.
676      * @param sets  New value of property sets.
677      */
678     public void setChangeSets( Collection sets )
679     {
680         this.sets = sets;
681     }
682 
683     /**
684      * Returns the developers.
685      * @return List
686      */
687     public List getDevelopers()
688     {
689         return developers;
690     }
691 
692     /**
693      * Sets the developers.
694      * @param developers The developers to set
695      */
696     public void setDevelopers( List developers )
697     {
698         this.developers = developers;
699     }
700 
701     /**
702      * Returns the outputEncoding.
703      * @return String
704      */
705     public String getOutputEncoding()
706     {
707         return outputEncoding;
708     }
709 
710     /**
711      * Sets the outputEncoding.
712      * @param outputEncoding The outputEncoding to set
713      */
714     public void setOutputEncoding( String outputEncoding )
715     {
716         this.outputEncoding = outputEncoding;
717     }
718 
719     /**
720      * Returns the commentFormat used to interrogate the RCS.
721      * @return String
722      */
723     public String getCommentFormat()
724     {
725         return commentFormat;
726     }
727 
728     /**
729      * Sets the commentFormat.
730      * @param commentFormat The commentFormat to set
731      */
732     public void setCommentFormat( String commentFormat )
733     {
734         this.commentFormat = commentFormat;
735     }
736 }
737  // end of ChangeLog