001package 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
022import java.io.Serializable;
023import java.text.ParseException;
024import java.text.SimpleDateFormat;
025import java.util.ArrayList;
026import java.util.Collections;
027import java.util.Date;
028import java.util.LinkedHashSet;
029import java.util.List;
030import java.util.Set;
031
032import org.apache.maven.scm.provider.ScmProviderRepository;
033import org.apache.maven.scm.util.FilenameUtils;
034import org.apache.maven.scm.util.ThreadSafeDateFormat;
035import org.codehaus.plexus.util.StringUtils;
036
037/**
038 * @author <a href="mailto:evenisse@apache.org">Emmanuel Venisse</a>
039 *
040 */
041public class ChangeSet
042    implements Serializable
043{
044    /**
045     *
046     */
047    private static final long serialVersionUID = 7097705862222539801L;
048
049    /**
050     * Escaped <code>&lt;</code> entity
051     */
052    public static final String LESS_THAN_ENTITY = "&lt;";
053
054    /**
055     * Escaped <code>&gt;</code> entity
056     */
057    public static final String GREATER_THAN_ENTITY = "&gt;";
058
059    /**
060     * Escaped <code>&amp;</code> entity
061     */
062    public static final String AMPERSAND_ENTITY = "&amp;";
063
064    /**
065     * Escaped <code>'</code> entity
066     */
067    public static final String APOSTROPHE_ENTITY = "&apos;";
068
069    /**
070     * Escaped <code>"</code> entity
071     */
072    public static final String QUOTE_ENTITY = "&quot;";
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>" );
540                    buffer.append( escapeValue( file.getOriginalName() ) );
541                    buffer.append( "</orig-name>\n" );
542                }
543                if ( file.getOriginalRevision() != null )
544                {
545                    buffer.append( "\t\t\t<orig-revision>" );
546                    buffer.append( file.getOriginalRevision() );
547                    buffer.append( "</orig-revision>\n" );
548                }
549                buffer.append( "\t\t</file>\n" );
550            }
551        }
552        buffer.append( "\t\t<msg><![CDATA[" )
553            .append( removeCDataEnd( comment ) )
554            .append( "]]></msg>\n" );
555        buffer.append( "\t</changelog-entry>\n" );
556
557        return buffer.toString();
558    }
559
560    /** {@inheritDoc} */
561    public boolean equals( Object obj )
562    {
563        if ( obj instanceof ChangeSet )
564        {
565            ChangeSet changeSet = (ChangeSet) obj;
566
567            if ( toString().equals( changeSet.toString() ) )
568            {
569                return true;
570            }
571        }
572
573        return false;
574    }
575
576    /** {@inheritDoc} */
577    public int hashCode()
578    {
579        final int prime = 31;
580        int result = 1;
581        result = prime * result + ( ( author == null ) ? 0 : author.hashCode() );
582        result = prime * result + ( ( comment == null ) ? 0 : comment.hashCode() );
583        result = prime * result + ( ( date == null ) ? 0 : date.hashCode() );
584        result = prime * result + ( ( parentRevision == null ) ? 0 : parentRevision.hashCode() );
585        result = prime * result + ( ( mergedRevisions == null ) ? 0 : mergedRevisions.hashCode() );
586        result = prime * result + ( ( files == null ) ? 0 : files.hashCode() );
587        return result;
588    }
589
590    /**
591     * remove a <code>]]></code> from comments (replace it with <code>] ] ></code>).
592     *
593     * @param message The message to modify
594     * @return a clean string
595     */
596    private String removeCDataEnd( String message )
597    {
598        // check for invalid sequence ]]>
599        int endCdata;
600        while ( message != null && ( endCdata = message.indexOf( "]]>" ) ) > -1 )
601        {
602            message = message.substring( 0, endCdata ) + "] ] >" + message.substring( endCdata + 3, message.length() );
603        }
604        return message;
605    }
606
607    /**
608     * <p>Escape the <code>toString</code> of the given object.
609     * For use in an attribute value.</p>
610     * <p/>
611     * swiped from jakarta-commons/betwixt -- XMLUtils.java
612     *
613     * @param value escape <code>value.toString()</code>
614     * @return text with characters restricted (for use in attributes) escaped
615     */
616    public static String escapeValue( Object value )
617    {
618        StringBuilder buffer = new StringBuilder( value.toString() );
619        for ( int i = 0, size = buffer.length(); i < size; i++ )
620        {
621            switch ( buffer.charAt( i ) )
622            {
623                case'<':
624                    buffer.replace( i, i + 1, LESS_THAN_ENTITY );
625                    size += 3;
626                    i += 3;
627                    break;
628                case'>':
629                    buffer.replace( i, i + 1, GREATER_THAN_ENTITY );
630                    size += 3;
631                    i += 3;
632                    break;
633                case'&':
634                    buffer.replace( i, i + 1, AMPERSAND_ENTITY );
635                    size += 4;
636                    i += 4;
637                    break;
638                case'\'':
639                    buffer.replace( i, i + 1, APOSTROPHE_ENTITY );
640                    size += 5;
641                    i += 5;
642                    break;
643                case'\"':
644                    buffer.replace( i, i + 1, QUOTE_ENTITY );
645                    size += 5;
646                    i += 5;
647                    break;
648                default:
649            }
650        }
651        return buffer.toString();
652    }
653}