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     * @version $Id: ChangeSet.java 1294391 2012-02-27 23:16:22Z hboutemy $
040     */
041    public 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>" ).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    }