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