1 package org.apache.maven.changelog;
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
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
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
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