001 package org.apache.maven.scm;
002
003 /*
004 * Licensed to the Apache Software Foundation (ASF) under one
005 * or more contributor license agreements. See the NOTICE file
006 * distributed with this work for additional information
007 * regarding copyright ownership. The ASF licenses this file
008 * to you under the Apache License, Version 2.0 (the
009 * "License"); you may not use this file except in compliance
010 * with the License. You may obtain a copy of the License at
011 *
012 * http://www.apache.org/licenses/LICENSE-2.0
013 *
014 * Unless required by applicable law or agreed to in writing,
015 * software distributed under the License is distributed on an
016 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
017 * KIND, either express or implied. See the License for the
018 * specific language governing permissions and limitations
019 * under the License.
020 */
021
022 import java.io.Serializable;
023 import java.text.ParseException;
024 import java.text.SimpleDateFormat;
025 import java.util.ArrayList;
026 import java.util.Collections;
027 import java.util.Date;
028 import java.util.LinkedHashSet;
029 import java.util.List;
030 import java.util.Set;
031
032 import org.apache.maven.scm.provider.ScmProviderRepository;
033 import org.apache.maven.scm.util.FilenameUtils;
034 import org.apache.maven.scm.util.ThreadSafeDateFormat;
035 import org.codehaus.plexus.util.StringUtils;
036
037 /**
038 * @author <a href="mailto:evenisse@apache.org">Emmanuel Venisse</a>
039 *
040 */
041 public class ChangeSet
042 implements Serializable
043 {
044 /**
045 *
046 */
047 private static final long serialVersionUID = 7097705862222539801L;
048
049 /**
050 * Escaped <code><</code> entity
051 */
052 public static final String LESS_THAN_ENTITY = "<";
053
054 /**
055 * Escaped <code>></code> entity
056 */
057 public static final String GREATER_THAN_ENTITY = ">";
058
059 /**
060 * Escaped <code>&</code> entity
061 */
062 public static final String AMPERSAND_ENTITY = "&";
063
064 /**
065 * Escaped <code>'</code> entity
066 */
067 public static final String APOSTROPHE_ENTITY = "'";
068
069 /**
070 * Escaped <code>"</code> entity
071 */
072 public static final String QUOTE_ENTITY = """;
073
074 private static final String DATE_PATTERN = "yyyy-MM-dd";
075
076 /**
077 * Formatter used by the getDateFormatted method.
078 */
079 private static final ThreadSafeDateFormat DATE_FORMAT = new ThreadSafeDateFormat( DATE_PATTERN );
080
081 private static final String TIME_PATTERN = "HH:mm:ss";
082
083 /**
084 * Formatter used by the getTimeFormatted method.
085 */
086 private static final ThreadSafeDateFormat TIME_FORMAT = new ThreadSafeDateFormat( TIME_PATTERN );
087
088 /**
089 * Formatter used to parse date/timestamp.
090 */
091 private static final ThreadSafeDateFormat TIMESTAMP_FORMAT_1 = new ThreadSafeDateFormat( "yyyy/MM/dd HH:mm:ss" );
092
093 private static final ThreadSafeDateFormat TIMESTAMP_FORMAT_2 = new ThreadSafeDateFormat( "yyyy-MM-dd HH:mm:ss" );
094
095 private static final ThreadSafeDateFormat TIMESTAMP_FORMAT_3 = new ThreadSafeDateFormat( "yyyy/MM/dd HH:mm:ss z" );
096
097 private static final ThreadSafeDateFormat TIMESTAMP_FORMAT_4 = new ThreadSafeDateFormat( "yyyy-MM-dd HH:mm:ss z" );
098
099 /**
100 * Date the changes were committed
101 */
102 private Date date;
103
104 /**
105 * User who made changes
106 */
107 private String author;
108
109 /**
110 * comment provided at commit time
111 */
112 private String comment = "";
113
114 /**
115 * List of ChangeFile
116 */
117 private List<ChangeFile> files;
118
119 /**
120 * The SCM revision id for this changeset.
121 * @since 1.3
122 */
123 private String revision;
124
125 /**
126 * Revision from which this one originates
127 * @since 1.7
128 */
129 private String parentRevision;
130
131 /**
132 * Revisions that were merged into this one
133 * @since 1.7
134 */
135 private Set<String> mergedRevisions;
136
137 /**
138 * @param strDate Date the changes were committed
139 * @param userDatePattern pattern of date
140 * @param comment comment provided at commit time
141 * @param author User who made changes
142 * @param files The ChangeFile list
143 */
144 public ChangeSet( String strDate, String userDatePattern, String comment, String author,
145 List<ChangeFile> files )
146 {
147 this( null, comment, author, files );
148
149 setDate( strDate, userDatePattern );
150 }
151
152 /**
153 * @param date Date the changes were committed
154 * @param comment comment provided at commit time
155 * @param author User who made changes
156 * @param files The ChangeFile list
157 */
158 public ChangeSet( Date date, String comment, String author, List<ChangeFile> files )
159 {
160 setDate( date );
161
162 setAuthor( author );
163
164 setComment( comment );
165
166 this.files = files;
167 }
168
169 /**
170 * Constructor used when attributes aren't available until later
171 */
172 public ChangeSet()
173 {
174 // no op
175 }
176
177 /**
178 * Getter for ChangeFile list.
179 *
180 * @return List of ChangeFile.
181 */
182 public List<ChangeFile> getFiles()
183 {
184 if ( files == null )
185 {
186 return new ArrayList<ChangeFile>();
187 }
188 return files;
189 }
190
191 /**
192 * Setter for ChangeFile list.
193 *
194 * @param files List of ChangeFiles.
195 */
196 public void setFiles( List<ChangeFile> files )
197 {
198 this.files = files;
199 }
200
201 public void addFile( ChangeFile file )
202 {
203 if ( files == null )
204 {
205 files = new ArrayList<ChangeFile>();
206 }
207
208 files.add( file );
209 }
210
211 /**
212 * @deprecated Use method {@link #containsFilename(String)}
213 * @param filename
214 * @param repository NOT USED
215 * @return
216 */
217 public boolean containsFilename( String filename, ScmProviderRepository repository )
218 {
219 return containsFilename( filename );
220 }
221
222 public boolean containsFilename( String filename )
223 {
224 if ( files != null )
225 {
226 for ( ChangeFile file : files )
227 {
228 String f1 = FilenameUtils.normalizeFilename( file.getName() );
229 String f2 = FilenameUtils.normalizeFilename( filename );
230 if ( f1.indexOf( f2 ) >= 0 )
231 {
232 return true;
233 }
234 }
235 }
236
237 return false;
238 }
239
240 /**
241 * Getter for property author.
242 *
243 * @return Value of property author.
244 */
245 public String getAuthor()
246 {
247 return author;
248 }
249
250 /**
251 * Setter for property author.
252 *
253 * @param author New value of property author.
254 */
255 public void setAuthor( String author )
256 {
257 this.author = author;
258 }
259
260 /**
261 * Getter for property comment.
262 *
263 * @return Value of property comment.
264 */
265 public String getComment()
266 {
267 return comment;
268 }
269
270 /**
271 * Setter for property comment.
272 *
273 * @param comment New value of property comment.
274 */
275 public void setComment( String comment )
276 {
277 this.comment = comment;
278 }
279
280 /**
281 * Getter for property date.
282 *
283 * @return Value of property date.
284 */
285 public Date getDate()
286 {
287 if ( date != null )
288 {
289 return (Date) date.clone();
290 }
291
292 return null;
293 }
294
295 /**
296 * Setter for property date.
297 *
298 * @param date New value of property date.
299 */
300 public void setDate( Date date )
301 {
302 if ( date != null )
303 {
304 this.date = new Date( date.getTime() );
305 }
306 }
307
308 /**
309 * Setter for property date that takes a string and parses it
310 *
311 * @param date - a string in yyyy/MM/dd HH:mm:ss format
312 */
313 public void setDate( String date )
314 {
315 setDate( date, null );
316 }
317
318 /**
319 * Setter for property date that takes a string and parses it
320 *
321 * @param date - a string in yyyy/MM/dd HH:mm:ss format
322 * @param userDatePattern - pattern of date
323 */
324 public void setDate( String date, String userDatePattern )
325 {
326 try
327 {
328 if ( !StringUtils.isEmpty( userDatePattern ) )
329 {
330 SimpleDateFormat format = new SimpleDateFormat( userDatePattern );
331
332 this.date = format.parse( date );
333 }
334 else
335 {
336 this.date = TIMESTAMP_FORMAT_3.parse( date );
337 }
338 }
339 catch ( ParseException e )
340 {
341 if ( !StringUtils.isEmpty( userDatePattern ) )
342 {
343 try
344 {
345 this.date = TIMESTAMP_FORMAT_3.parse( date );
346 }
347 catch ( ParseException pe )
348 {
349 try
350 {
351 this.date = TIMESTAMP_FORMAT_4.parse( date );
352 }
353 catch ( ParseException pe1 )
354 {
355 try
356 {
357 this.date = TIMESTAMP_FORMAT_1.parse( date );
358 }
359 catch ( ParseException pe2 )
360 {
361 try
362 {
363 this.date = TIMESTAMP_FORMAT_2.parse( date );
364 }
365 catch ( ParseException pe3 )
366 {
367 throw new IllegalArgumentException( "Unable to parse date: " + date );
368 }
369 }
370 }
371 }
372 }
373 else
374 {
375 try
376 {
377 this.date = TIMESTAMP_FORMAT_4.parse( date );
378 }
379 catch ( ParseException pe1 )
380 {
381 try
382 {
383 this.date = TIMESTAMP_FORMAT_1.parse( date );
384 }
385 catch ( ParseException pe2 )
386 {
387 try
388 {
389 this.date = TIMESTAMP_FORMAT_2.parse( date );
390 }
391 catch ( ParseException pe3 )
392 {
393 throw new IllegalArgumentException( "Unable to parse date: " + date );
394 }
395 }
396 }
397 }
398 }
399 }
400
401 /**
402 * @return date in yyyy-mm-dd format
403 */
404 public String getDateFormatted()
405 {
406 return DATE_FORMAT.format( getDate() );
407 }
408
409 /**
410 * @return time in HH:mm:ss format
411 */
412 public String getTimeFormatted()
413 {
414 return TIME_FORMAT.format( getDate() );
415 }
416
417 /**
418 * @return
419 * @since 1.3
420 */
421 public String getRevision()
422 {
423 return revision;
424 }
425
426 /**
427 * @param revision
428 * @since 1.3
429 */
430 public void setRevision( String revision )
431 {
432 this.revision = revision;
433 }
434
435 public String getParentRevision()
436 {
437 return parentRevision;
438 }
439
440 public void setParentRevision( String parentRevision )
441 {
442 this.parentRevision = parentRevision;
443 }
444
445 public void addMergedRevision( String mergedRevision )
446 {
447 if ( mergedRevisions == null )
448 {
449 mergedRevisions = new LinkedHashSet<String>();
450 }
451 mergedRevisions.add( mergedRevision );
452 }
453
454 public Set<String> getMergedRevisions()
455 {
456 return mergedRevisions == null ? Collections.<String> emptySet() : mergedRevisions;
457 }
458
459 public void setMergedRevisions( Set<String> mergedRevisions )
460 {
461 this.mergedRevisions = mergedRevisions;
462 }
463
464 /** {@inheritDoc} */
465 public String toString()
466 {
467 StringBuilder result = new StringBuilder( author == null ? " null " : author );
468 result.append( "\n" ).append( date == null ? "null " : date.toString() ).append( "\n" );
469 // parent(s)
470 if ( parentRevision != null )
471 {
472 result.append( "parent: " ).append( parentRevision );
473 if ( !mergedRevisions.isEmpty() )
474 {
475 result.append( " + " );
476 result.append( mergedRevisions );
477 }
478 result.append( "\n" );
479 }
480 if ( files != null )
481 {
482 for ( ChangeFile file : files )
483 {
484 result.append( file == null ? " null " : file.toString() ).append( "\n" );
485 }
486 }
487
488 result.append( comment == null ? " null " : comment );
489
490 return result.toString();
491 }
492
493 /**
494 * Provide the changelog entry as an XML snippet.
495 *
496 * @return a changelog-entry in xml format
497 * @task make sure comment doesn't contain CDATA tags - MAVEN114
498 */
499 public String toXML()
500 {
501 StringBuilder buffer = new StringBuilder("\t<changelog-entry>\n" );
502
503 if ( getDate() != null )
504 {
505 buffer.append( "\t\t<date pattern=\"" + DATE_PATTERN + "\">" )
506 .append( getDateFormatted() )
507 .append( "</date>\n" )
508 .append( "\t\t<time pattern=\"" + TIME_PATTERN + "\">" )
509 .append( getTimeFormatted() )
510 .append( "</time>\n" );
511 }
512
513 buffer.append( "\t\t<author><![CDATA[" )
514 .append( author )
515 .append( "]]></author>\n" );
516
517 if ( parentRevision != null )
518 {
519 buffer.append( "\t\t<parent>" ).append( getParentRevision() ).append( "</parent>\n" );
520 }
521 for ( String mergedRevision : getMergedRevisions() )
522 {
523 buffer.append( "\t\t<merge>" ).append( mergedRevision ).append( "</merge>\n" );
524 }
525
526 if ( files != null )
527 {
528 for ( ChangeFile file : files )
529 {
530 buffer.append( "\t\t<file>\n" );
531 if ( file.getAction() != null )
532 {
533 buffer.append( "\t\t\t<action>" ).append( file.getAction() ).append( "</action>\n" );
534 }
535 buffer.append( "\t\t\t<name>" ).append( escapeValue( file.getName() ) ).append( "</name>\n" );
536 buffer.append( "\t\t\t<revision>" ).append( file.getRevision() ).append( "</revision>\n" );
537 if ( file.getOriginalName() != null )
538 {
539 buffer.append( "\t\t\t<orig-name>" ).append( escapeValue( file.getOriginalName() ) ).append( "</orig-name>\n" );
540 }
541 if ( file.getOriginalRevision() != null )
542 {
543 buffer.append( "\t\t\t<orig-revision>" ).append( file.getOriginalRevision() ).append( "</orig-revision>\n" );
544 }
545 buffer.append( "\t\t</file>\n" );
546 }
547 }
548 buffer.append( "\t\t<msg><![CDATA[" )
549 .append( removeCDataEnd( comment ) )
550 .append( "]]></msg>\n" );
551 buffer.append( "\t</changelog-entry>\n" );
552
553 return buffer.toString();
554 }
555
556 /** {@inheritDoc} */
557 public boolean equals( Object obj )
558 {
559 if ( obj instanceof ChangeSet )
560 {
561 ChangeSet changeSet = (ChangeSet) obj;
562
563 if ( toString().equals( changeSet.toString() ) )
564 {
565 return true;
566 }
567 }
568
569 return false;
570 }
571
572 /** {@inheritDoc} */
573 public int hashCode()
574 {
575 final int prime = 31;
576 int result = 1;
577 result = prime * result + ( ( author == null ) ? 0 : author.hashCode() );
578 result = prime * result + ( ( comment == null ) ? 0 : comment.hashCode() );
579 result = prime * result + ( ( date == null ) ? 0 : date.hashCode() );
580 result = prime * result + ( ( parentRevision == null ) ? 0 : parentRevision.hashCode() );
581 result = prime * result + ( ( mergedRevisions == null ) ? 0 : mergedRevisions.hashCode() );
582 result = prime * result + ( ( files == null ) ? 0 : files.hashCode() );
583 return result;
584 }
585
586 /**
587 * remove a <code>]]></code> from comments (replace it with <code>] ] ></code>).
588 *
589 * @param message The message to modify
590 * @return a clean string
591 */
592 private String removeCDataEnd( String message )
593 {
594 // check for invalid sequence ]]>
595 int endCdata;
596 while ( message != null && ( endCdata = message.indexOf( "]]>" ) ) > -1 )
597 {
598 message = message.substring( 0, endCdata ) + "] ] >" + message.substring( endCdata + 3, message.length() );
599 }
600 return message;
601 }
602
603 /**
604 * <p>Escape the <code>toString</code> of the given object.
605 * For use in an attribute value.</p>
606 * <p/>
607 * swiped from jakarta-commons/betwixt -- XMLUtils.java
608 *
609 * @param value escape <code>value.toString()</code>
610 * @return text with characters restricted (for use in attributes) escaped
611 */
612 public static String escapeValue( Object value )
613 {
614 StringBuilder buffer = new StringBuilder( value.toString() );
615 for ( int i = 0, size = buffer.length(); i < size; i++ )
616 {
617 switch ( buffer.charAt( i ) )
618 {
619 case'<':
620 buffer.replace( i, i + 1, LESS_THAN_ENTITY );
621 size += 3;
622 i += 3;
623 break;
624 case'>':
625 buffer.replace( i, i + 1, GREATER_THAN_ENTITY );
626 size += 3;
627 i += 3;
628 break;
629 case'&':
630 buffer.replace( i, i + 1, AMPERSAND_ENTITY );
631 size += 4;
632 i += 4;
633 break;
634 case'\'':
635 buffer.replace( i, i + 1, APOSTROPHE_ENTITY );
636 size += 5;
637 i += 5;
638 break;
639 case'\"':
640 buffer.replace( i, i + 1, QUOTE_ENTITY );
641 size += 5;
642 i += 5;
643 break;
644 }
645 }
646 return buffer.toString();
647 }
648 }