| package org.apache.maven.scm; |
| |
| /* |
| * Licensed to the Apache Software Foundation (ASF) under one |
| * or more contributor license agreements. See the NOTICE file |
| * distributed with this work for additional information |
| * regarding copyright ownership. The ASF licenses this file |
| * to you under the Apache License, Version 2.0 (the |
| * "License"); you may not use this file except in compliance |
| * with the License. You may obtain a copy of the License at |
| * |
| * http://www.apache.org/licenses/LICENSE-2.0 |
| * |
| * Unless required by applicable law or agreed to in writing, |
| * software distributed under the License is distributed on an |
| * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY |
| * KIND, either express or implied. See the License for the |
| * specific language governing permissions and limitations |
| * under the License. |
| */ |
| |
| import java.io.Serializable; |
| import java.text.ParseException; |
| import java.text.SimpleDateFormat; |
| import java.util.ArrayList; |
| import java.util.Collections; |
| import java.util.Date; |
| import java.util.LinkedHashSet; |
| import java.util.List; |
| import java.util.Set; |
| |
| import org.apache.maven.scm.provider.ScmProviderRepository; |
| import org.apache.maven.scm.util.FilenameUtils; |
| import org.apache.maven.scm.util.ThreadSafeDateFormat; |
| import org.codehaus.plexus.util.StringUtils; |
| |
| /** |
| * @author <a href="mailto:evenisse@apache.org">Emmanuel Venisse</a> |
| * |
| */ |
| public class ChangeSet |
| implements Serializable |
| { |
| /** |
| * |
| */ |
| private static final long serialVersionUID = 7097705862222539801L; |
| |
| /** |
| * Escaped <code><</code> entity |
| */ |
| public static final String LESS_THAN_ENTITY = "<"; |
| |
| /** |
| * Escaped <code>></code> entity |
| */ |
| public static final String GREATER_THAN_ENTITY = ">"; |
| |
| /** |
| * Escaped <code>&</code> entity |
| */ |
| public static final String AMPERSAND_ENTITY = "&"; |
| |
| /** |
| * Escaped <code>'</code> entity |
| */ |
| public static final String APOSTROPHE_ENTITY = "'"; |
| |
| /** |
| * Escaped <code>"</code> entity |
| */ |
| public static final String QUOTE_ENTITY = """; |
| |
| private static final String DATE_PATTERN = "yyyy-MM-dd"; |
| |
| /** |
| * Formatter used by the getDateFormatted method. |
| */ |
| private static final ThreadSafeDateFormat DATE_FORMAT = new ThreadSafeDateFormat( DATE_PATTERN ); |
| |
| private static final String TIME_PATTERN = "HH:mm:ss"; |
| |
| /** |
| * Formatter used by the getTimeFormatted method. |
| */ |
| private static final ThreadSafeDateFormat TIME_FORMAT = new ThreadSafeDateFormat( TIME_PATTERN ); |
| |
| /** |
| * Formatter used to parse date/timestamp. |
| */ |
| private static final ThreadSafeDateFormat TIMESTAMP_FORMAT_1 = new ThreadSafeDateFormat( "yyyy/MM/dd HH:mm:ss" ); |
| |
| private static final ThreadSafeDateFormat TIMESTAMP_FORMAT_2 = new ThreadSafeDateFormat( "yyyy-MM-dd HH:mm:ss" ); |
| |
| private static final ThreadSafeDateFormat TIMESTAMP_FORMAT_3 = new ThreadSafeDateFormat( "yyyy/MM/dd HH:mm:ss z" ); |
| |
| private static final ThreadSafeDateFormat TIMESTAMP_FORMAT_4 = new ThreadSafeDateFormat( "yyyy-MM-dd HH:mm:ss z" ); |
| |
| /** |
| * Date the changes were committed |
| */ |
| private Date date; |
| |
| /** |
| * User who made changes |
| */ |
| private String author; |
| |
| /** |
| * comment provided at commit time |
| */ |
| private String comment = ""; |
| |
| /** |
| * List of ChangeFile |
| */ |
| private List<ChangeFile> files; |
| |
| /** |
| * The SCM revision id for this changeset. |
| * @since 1.3 |
| */ |
| private String revision; |
| |
| /** |
| * Revision from which this one originates |
| * @since 1.7 |
| */ |
| private String parentRevision; |
| |
| /** |
| * Revisions that were merged into this one |
| * @since 1.7 |
| */ |
| private Set<String> mergedRevisions; |
| |
| /** |
| * @param strDate Date the changes were committed |
| * @param userDatePattern pattern of date |
| * @param comment comment provided at commit time |
| * @param author User who made changes |
| * @param files The ChangeFile list |
| */ |
| public ChangeSet( String strDate, String userDatePattern, String comment, String author, |
| List<ChangeFile> files ) |
| { |
| this( null, comment, author, files ); |
| |
| setDate( strDate, userDatePattern ); |
| } |
| |
| /** |
| * @param date Date the changes were committed |
| * @param comment comment provided at commit time |
| * @param author User who made changes |
| * @param files The ChangeFile list |
| */ |
| public ChangeSet( Date date, String comment, String author, List<ChangeFile> files ) |
| { |
| setDate( date ); |
| |
| setAuthor( author ); |
| |
| setComment( comment ); |
| |
| this.files = files; |
| } |
| |
| /** |
| * Constructor used when attributes aren't available until later |
| */ |
| public ChangeSet() |
| { |
| // no op |
| } |
| |
| /** |
| * Getter for ChangeFile list. |
| * |
| * @return List of ChangeFile. |
| */ |
| public List<ChangeFile> getFiles() |
| { |
| if ( files == null ) |
| { |
| return new ArrayList<ChangeFile>(); |
| } |
| return files; |
| } |
| |
| /** |
| * Setter for ChangeFile list. |
| * |
| * @param files List of ChangeFiles. |
| */ |
| public void setFiles( List<ChangeFile> files ) |
| { |
| this.files = files; |
| } |
| |
| public void addFile( ChangeFile file ) |
| { |
| if ( files == null ) |
| { |
| files = new ArrayList<ChangeFile>(); |
| } |
| |
| files.add( file ); |
| } |
| |
| /** |
| * @deprecated Use method {@link #containsFilename(String)} |
| * @param filename |
| * @param repository NOT USED |
| * @return |
| */ |
| public boolean containsFilename( String filename, ScmProviderRepository repository ) |
| { |
| return containsFilename( filename ); |
| } |
| |
| public boolean containsFilename( String filename ) |
| { |
| if ( files != null ) |
| { |
| for ( ChangeFile file : files ) |
| { |
| String f1 = FilenameUtils.normalizeFilename( file.getName() ); |
| String f2 = FilenameUtils.normalizeFilename( filename ); |
| if ( f1.indexOf( f2 ) >= 0 ) |
| { |
| return true; |
| } |
| } |
| } |
| |
| return false; |
| } |
| |
| /** |
| * Getter for property author. |
| * |
| * @return Value of property author. |
| */ |
| public String getAuthor() |
| { |
| return author; |
| } |
| |
| /** |
| * Setter for property author. |
| * |
| * @param author New value of property author. |
| */ |
| public void setAuthor( String author ) |
| { |
| this.author = author; |
| } |
| |
| /** |
| * Getter for property comment. |
| * |
| * @return Value of property comment. |
| */ |
| public String getComment() |
| { |
| return comment; |
| } |
| |
| /** |
| * Setter for property comment. |
| * |
| * @param comment New value of property comment. |
| */ |
| public void setComment( String comment ) |
| { |
| this.comment = comment; |
| } |
| |
| /** |
| * Getter for property date. |
| * |
| * @return Value of property date. |
| */ |
| public Date getDate() |
| { |
| if ( date != null ) |
| { |
| return (Date) date.clone(); |
| } |
| |
| return null; |
| } |
| |
| /** |
| * Setter for property date. |
| * |
| * @param date New value of property date. |
| */ |
| public void setDate( Date date ) |
| { |
| if ( date != null ) |
| { |
| this.date = new Date( date.getTime() ); |
| } |
| } |
| |
| /** |
| * Setter for property date that takes a string and parses it |
| * |
| * @param date - a string in yyyy/MM/dd HH:mm:ss format |
| */ |
| public void setDate( String date ) |
| { |
| setDate( date, null ); |
| } |
| |
| /** |
| * Setter for property date that takes a string and parses it |
| * |
| * @param date - a string in yyyy/MM/dd HH:mm:ss format |
| * @param userDatePattern - pattern of date |
| */ |
| public void setDate( String date, String userDatePattern ) |
| { |
| try |
| { |
| if ( !StringUtils.isEmpty( userDatePattern ) ) |
| { |
| SimpleDateFormat format = new SimpleDateFormat( userDatePattern ); |
| |
| this.date = format.parse( date ); |
| } |
| else |
| { |
| this.date = TIMESTAMP_FORMAT_3.parse( date ); |
| } |
| } |
| catch ( ParseException e ) |
| { |
| if ( !StringUtils.isEmpty( userDatePattern ) ) |
| { |
| try |
| { |
| this.date = TIMESTAMP_FORMAT_3.parse( date ); |
| } |
| catch ( ParseException pe ) |
| { |
| try |
| { |
| this.date = TIMESTAMP_FORMAT_4.parse( date ); |
| } |
| catch ( ParseException pe1 ) |
| { |
| try |
| { |
| this.date = TIMESTAMP_FORMAT_1.parse( date ); |
| } |
| catch ( ParseException pe2 ) |
| { |
| try |
| { |
| this.date = TIMESTAMP_FORMAT_2.parse( date ); |
| } |
| catch ( ParseException pe3 ) |
| { |
| throw new IllegalArgumentException( "Unable to parse date: " + date ); |
| } |
| } |
| } |
| } |
| } |
| else |
| { |
| try |
| { |
| this.date = TIMESTAMP_FORMAT_4.parse( date ); |
| } |
| catch ( ParseException pe1 ) |
| { |
| try |
| { |
| this.date = TIMESTAMP_FORMAT_1.parse( date ); |
| } |
| catch ( ParseException pe2 ) |
| { |
| try |
| { |
| this.date = TIMESTAMP_FORMAT_2.parse( date ); |
| } |
| catch ( ParseException pe3 ) |
| { |
| throw new IllegalArgumentException( "Unable to parse date: " + date ); |
| } |
| } |
| } |
| } |
| } |
| } |
| |
| /** |
| * @return date in yyyy-mm-dd format |
| */ |
| public String getDateFormatted() |
| { |
| return DATE_FORMAT.format( getDate() ); |
| } |
| |
| /** |
| * @return time in HH:mm:ss format |
| */ |
| public String getTimeFormatted() |
| { |
| return TIME_FORMAT.format( getDate() ); |
| } |
| |
| /** |
| * @return |
| * @since 1.3 |
| */ |
| public String getRevision() |
| { |
| return revision; |
| } |
| |
| /** |
| * @param revision |
| * @since 1.3 |
| */ |
| public void setRevision( String revision ) |
| { |
| this.revision = revision; |
| } |
| |
| public String getParentRevision() |
| { |
| return parentRevision; |
| } |
| |
| public void setParentRevision( String parentRevision ) |
| { |
| this.parentRevision = parentRevision; |
| } |
| |
| public void addMergedRevision( String mergedRevision ) |
| { |
| if ( mergedRevisions == null ) |
| { |
| mergedRevisions = new LinkedHashSet<String>(); |
| } |
| mergedRevisions.add( mergedRevision ); |
| } |
| |
| public Set<String> getMergedRevisions() |
| { |
| return mergedRevisions == null ? Collections.<String> emptySet() : mergedRevisions; |
| } |
| |
| public void setMergedRevisions( Set<String> mergedRevisions ) |
| { |
| this.mergedRevisions = mergedRevisions; |
| } |
| |
| /** {@inheritDoc} */ |
| public String toString() |
| { |
| StringBuilder result = new StringBuilder( author == null ? " null " : author ); |
| result.append( "\n" ).append( date == null ? "null " : date.toString() ).append( "\n" ); |
| // parent(s) |
| if ( parentRevision != null ) |
| { |
| result.append( "parent: " ).append( parentRevision ); |
| if ( !mergedRevisions.isEmpty() ) |
| { |
| result.append( " + " ); |
| result.append( mergedRevisions ); |
| } |
| result.append( "\n" ); |
| } |
| if ( files != null ) |
| { |
| for ( ChangeFile file : files ) |
| { |
| result.append( file == null ? " null " : file.toString() ).append( "\n" ); |
| } |
| } |
| |
| result.append( comment == null ? " null " : comment ); |
| |
| return result.toString(); |
| } |
| |
| /** |
| * Provide the changelog entry as an XML snippet. |
| * |
| * @return a changelog-entry in xml format |
| * @task make sure comment doesn't contain CDATA tags - MAVEN114 |
| */ |
| public String toXML() |
| { |
| StringBuilder buffer = new StringBuilder("\t<changelog-entry>\n" ); |
| |
| if ( getDate() != null ) |
| { |
| buffer.append( "\t\t<date pattern=\"" + DATE_PATTERN + "\">" ) |
| .append( getDateFormatted() ) |
| .append( "</date>\n" ) |
| .append( "\t\t<time pattern=\"" + TIME_PATTERN + "\">" ) |
| .append( getTimeFormatted() ) |
| .append( "</time>\n" ); |
| } |
| |
| buffer.append( "\t\t<author><![CDATA[" ) |
| .append( author ) |
| .append( "]]></author>\n" ); |
| |
| if ( parentRevision != null ) |
| { |
| buffer.append( "\t\t<parent>" ).append( getParentRevision() ).append( "</parent>\n" ); |
| } |
| for ( String mergedRevision : getMergedRevisions() ) |
| { |
| buffer.append( "\t\t<merge>" ).append( mergedRevision ).append( "</merge>\n" ); |
| } |
| |
| if ( files != null ) |
| { |
| for ( ChangeFile file : files ) |
| { |
| buffer.append( "\t\t<file>\n" ); |
| if ( file.getAction() != null ) |
| { |
| buffer.append( "\t\t\t<action>" ).append( file.getAction() ).append( "</action>\n" ); |
| } |
| buffer.append( "\t\t\t<name>" ).append( escapeValue( file.getName() ) ).append( "</name>\n" ); |
| buffer.append( "\t\t\t<revision>" ).append( file.getRevision() ).append( "</revision>\n" ); |
| if ( file.getOriginalName() != null ) |
| { |
| buffer.append( "\t\t\t<orig-name>" ).append( escapeValue( file.getOriginalName() ) ).append( "</orig-name>\n" ); |
| } |
| if ( file.getOriginalRevision() != null ) |
| { |
| buffer.append( "\t\t\t<orig-revision>" ).append( file.getOriginalRevision() ).append( "</orig-revision>\n" ); |
| } |
| buffer.append( "\t\t</file>\n" ); |
| } |
| } |
| buffer.append( "\t\t<msg><![CDATA[" ) |
| .append( removeCDataEnd( comment ) ) |
| .append( "]]></msg>\n" ); |
| buffer.append( "\t</changelog-entry>\n" ); |
| |
| return buffer.toString(); |
| } |
| |
| /** {@inheritDoc} */ |
| public boolean equals( Object obj ) |
| { |
| if ( obj instanceof ChangeSet ) |
| { |
| ChangeSet changeSet = (ChangeSet) obj; |
| |
| if ( toString().equals( changeSet.toString() ) ) |
| { |
| return true; |
| } |
| } |
| |
| return false; |
| } |
| |
| /** {@inheritDoc} */ |
| public int hashCode() |
| { |
| final int prime = 31; |
| int result = 1; |
| result = prime * result + ( ( author == null ) ? 0 : author.hashCode() ); |
| result = prime * result + ( ( comment == null ) ? 0 : comment.hashCode() ); |
| result = prime * result + ( ( date == null ) ? 0 : date.hashCode() ); |
| result = prime * result + ( ( parentRevision == null ) ? 0 : parentRevision.hashCode() ); |
| result = prime * result + ( ( mergedRevisions == null ) ? 0 : mergedRevisions.hashCode() ); |
| result = prime * result + ( ( files == null ) ? 0 : files.hashCode() ); |
| return result; |
| } |
| |
| /** |
| * remove a <code>]]></code> from comments (replace it with <code>] ] ></code>). |
| * |
| * @param message The message to modify |
| * @return a clean string |
| */ |
| private String removeCDataEnd( String message ) |
| { |
| // check for invalid sequence ]]> |
| int endCdata; |
| while ( message != null && ( endCdata = message.indexOf( "]]>" ) ) > -1 ) |
| { |
| message = message.substring( 0, endCdata ) + "] ] >" + message.substring( endCdata + 3, message.length() ); |
| } |
| return message; |
| } |
| |
| /** |
| * <p>Escape the <code>toString</code> of the given object. |
| * For use in an attribute value.</p> |
| * <p/> |
| * swiped from jakarta-commons/betwixt -- XMLUtils.java |
| * |
| * @param value escape <code>value.toString()</code> |
| * @return text with characters restricted (for use in attributes) escaped |
| */ |
| public static String escapeValue( Object value ) |
| { |
| StringBuilder buffer = new StringBuilder( value.toString() ); |
| for ( int i = 0, size = buffer.length(); i < size; i++ ) |
| { |
| switch ( buffer.charAt( i ) ) |
| { |
| case'<': |
| buffer.replace( i, i + 1, LESS_THAN_ENTITY ); |
| size += 3; |
| i += 3; |
| break; |
| case'>': |
| buffer.replace( i, i + 1, GREATER_THAN_ENTITY ); |
| size += 3; |
| i += 3; |
| break; |
| case'&': |
| buffer.replace( i, i + 1, AMPERSAND_ENTITY ); |
| size += 4; |
| i += 4; |
| break; |
| case'\'': |
| buffer.replace( i, i + 1, APOSTROPHE_ENTITY ); |
| size += 5; |
| i += 5; |
| break; |
| case'\"': |
| buffer.replace( i, i + 1, QUOTE_ENTITY ); |
| size += 5; |
| i += 5; |
| break; |
| } |
| } |
| return buffer.toString(); |
| } |
| } |