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; 034 035/** 036 * @author <a href="mailto:evenisse@apache.org">Emmanuel Venisse</a> 037 */ 038public class ChangeSet implements Serializable { 039 private static final long serialVersionUID = 7097705862222539801L; 040 041 /** 042 * Escaped <code><</code> entity. 043 */ 044 public static final String LESS_THAN_ENTITY = "<"; 045 046 /** 047 * Escaped <code>></code> entity. 048 */ 049 public static final String GREATER_THAN_ENTITY = ">"; 050 051 /** 052 * Escaped <code>&</code> entity. 053 */ 054 public static final String AMPERSAND_ENTITY = "&"; 055 056 /** 057 * Escaped <code>'</code> entity. 058 */ 059 public static final String APOSTROPHE_ENTITY = "'"; 060 061 /** 062 * Escaped <code>"</code> entity. 063 */ 064 public static final String QUOTE_ENTITY = """; 065 066 private static final String DATE_PATTERN = "yyyy-MM-dd"; 067 068 /** 069 * Formatter used by the getDateFormatted method. 070 */ 071 private static final ThreadSafeDateFormat DATE_FORMAT = new ThreadSafeDateFormat(DATE_PATTERN); 072 073 private static final String TIME_PATTERN = "HH:mm:ss"; 074 075 /** 076 * Formatter used by the getTimeFormatted method. 077 */ 078 private static final ThreadSafeDateFormat TIME_FORMAT = new ThreadSafeDateFormat(TIME_PATTERN); 079 080 /** 081 * Formatter used to parse date/timestamp. 082 */ 083 private static final ThreadSafeDateFormat TIMESTAMP_FORMAT_1 = new ThreadSafeDateFormat("yyyy/MM/dd HH:mm:ss"); 084 085 private static final ThreadSafeDateFormat TIMESTAMP_FORMAT_2 = new ThreadSafeDateFormat("yyyy-MM-dd HH:mm:ss"); 086 087 private static final ThreadSafeDateFormat TIMESTAMP_FORMAT_3 = new ThreadSafeDateFormat("yyyy/MM/dd HH:mm:ss z"); 088 089 private static final ThreadSafeDateFormat TIMESTAMP_FORMAT_4 = new ThreadSafeDateFormat("yyyy-MM-dd HH:mm:ss z"); 090 091 /** 092 * Date the changes were committed. 093 */ 094 private Date date; 095 096 /** 097 * User who made changes. 098 */ 099 private String author; 100 101 /** 102 * Comment provided at commit time. 103 */ 104 private String comment = ""; 105 106 /** 107 * List of ChangeFile. 108 */ 109 private List<ChangeFile> files; 110 111 /** 112 * List of tags. 113 */ 114 private List<String> tags; 115 116 /** 117 * The SCM revision id for this changeset. 118 * 119 * @since 1.3 120 */ 121 private String revision; 122 123 /** 124 * Revision from which this one originates. 125 * 126 * @since 1.7 127 */ 128 private String parentRevision; 129 130 /** 131 * Revisions that were merged into this one. 132 * 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, List<ChangeFile> files) { 145 this(null, comment, author, files); 146 147 setDate(strDate, userDatePattern); 148 } 149 150 /** 151 * @param date date the changes were committed 152 * @param comment comment provided at commit time 153 * @param author user who made changes 154 * @param files the ChangeFile list 155 */ 156 public ChangeSet(Date date, String comment, String author, List<ChangeFile> files) { 157 setDate(date); 158 159 setAuthor(author); 160 161 setComment(comment); 162 163 this.files = files; 164 } 165 166 /** 167 * Constructor used when attributes aren't available until later. 168 */ 169 public ChangeSet() { 170 // no op 171 } 172 173 /** 174 * Getter for ChangeFile list. 175 * 176 * @return list of ChangeFile 177 */ 178 public List<ChangeFile> getFiles() { 179 if (files == null) { 180 return new ArrayList<>(); 181 } 182 return files; 183 } 184 185 /** 186 * Setter for ChangeFile list. 187 * 188 * @param files list of ChangeFiles 189 */ 190 public void setFiles(List<ChangeFile> files) { 191 this.files = files; 192 } 193 194 public void addFile(ChangeFile file) { 195 if (files == null) { 196 files = new ArrayList<>(); 197 } 198 199 files.add(file); 200 } 201 202 /** 203 * @param filename TODO 204 * @param repository NOT USED 205 * @return TODO 206 * @deprecated use method {@link #containsFilename(String)} 207 */ 208 public boolean containsFilename(String filename, ScmProviderRepository repository) { 209 return containsFilename(filename); 210 } 211 212 public boolean containsFilename(String filename) { 213 if (files != null) { 214 for (ChangeFile file : files) { 215 String f1 = FilenameUtils.normalizeFilename(file.getName()); 216 String f2 = FilenameUtils.normalizeFilename(filename); 217 if (f1.indexOf(f2) >= 0) { 218 return true; 219 } 220 } 221 } 222 223 return false; 224 } 225 226 /** 227 * Getter for property author. 228 * 229 * @return value of property author 230 */ 231 public String getAuthor() { 232 return author; 233 } 234 235 /** 236 * Setter for property author. 237 * 238 * @param author new value of property author 239 */ 240 public void setAuthor(String author) { 241 this.author = author; 242 } 243 244 /** 245 * Getter for property comment. 246 * 247 * @return value of property comment 248 */ 249 public String getComment() { 250 return comment; 251 } 252 253 /** 254 * Setter for property comment. 255 * 256 * @param comment new value of property comment 257 */ 258 public void setComment(String comment) { 259 this.comment = comment; 260 } 261 262 /** 263 * Getter for property date. 264 * 265 * @return value of property date 266 */ 267 public Date getDate() { 268 if (date != null) { 269 return (Date) date.clone(); 270 } 271 272 return null; 273 } 274 275 /** 276 * Setter for property date. 277 * 278 * @param date new value of property date 279 */ 280 public void setDate(Date date) { 281 if (date != null) { 282 this.date = new Date(date.getTime()); 283 } 284 } 285 286 /** 287 * Setter for property date that takes a string and parses it. 288 * 289 * @param date a string in yyyy/MM/dd HH:mm:ss format 290 */ 291 public void setDate(String date) { 292 setDate(date, null); 293 } 294 295 /** 296 * Setter for property date that takes a string and parses it. 297 * 298 * @param date a string in yyyy/MM/dd HH:mm:ss format 299 * @param userDatePattern pattern of date 300 */ 301 public void setDate(String date, String userDatePattern) { 302 try { 303 if (!(userDatePattern == null || userDatePattern.isEmpty())) { 304 SimpleDateFormat format = new SimpleDateFormat(userDatePattern); 305 306 this.date = format.parse(date); 307 } else { 308 this.date = TIMESTAMP_FORMAT_3.parse(date); 309 } 310 } catch (ParseException e) { 311 if (!(userDatePattern == null || userDatePattern.isEmpty())) { 312 try { 313 this.date = TIMESTAMP_FORMAT_3.parse(date); 314 } catch (ParseException pe) { 315 try { 316 this.date = TIMESTAMP_FORMAT_4.parse(date); 317 } catch (ParseException pe1) { 318 try { 319 this.date = TIMESTAMP_FORMAT_1.parse(date); 320 } catch (ParseException pe2) { 321 try { 322 this.date = TIMESTAMP_FORMAT_2.parse(date); 323 } catch (ParseException pe3) { 324 throw new IllegalArgumentException("Unable to parse date: " + date); 325 } 326 } 327 } 328 } 329 } else { 330 try { 331 this.date = TIMESTAMP_FORMAT_4.parse(date); 332 } catch (ParseException pe1) { 333 try { 334 this.date = TIMESTAMP_FORMAT_1.parse(date); 335 } catch (ParseException pe2) { 336 try { 337 this.date = TIMESTAMP_FORMAT_2.parse(date); 338 } catch (ParseException pe3) { 339 throw new IllegalArgumentException("Unable to parse date: " + date); 340 } 341 } 342 } 343 } 344 } 345 } 346 347 /** 348 * @return date in yyyy-mm-dd format 349 */ 350 public String getDateFormatted() { 351 return DATE_FORMAT.format(getDate()); 352 } 353 354 /** 355 * @return time in HH:mm:ss format 356 */ 357 public String getTimeFormatted() { 358 return TIME_FORMAT.format(getDate()); 359 } 360 361 /** 362 * Getter for property tags. 363 * 364 * @return value of property author 365 */ 366 public List<String> getTags() { 367 if (tags == null) { 368 return new ArrayList<>(); 369 } 370 return tags; 371 } 372 373 /** 374 * Setter for property tags. 375 * 376 * @param tags new value of property tags. This replaces the existing list (if any). 377 */ 378 public void setTags(List<String> tags) { 379 this.tags = tags; 380 } 381 382 /** 383 * Setter for property tags. 384 * 385 * @param tag new tag to add to the list of tags 386 */ 387 public void addTag(String tag) { 388 if (tag == null) { 389 return; 390 } 391 tag = tag.trim(); 392 if (tag.isEmpty()) { 393 return; 394 } 395 if (tags == null) { 396 tags = new ArrayList<>(); 397 } 398 tags.add(tag); 399 } 400 401 /** 402 * @return TODO 403 * @since 1.3 404 */ 405 public String getRevision() { 406 return revision; 407 } 408 409 /** 410 * @param revision TODO 411 * @since 1.3 412 */ 413 public void setRevision(String revision) { 414 this.revision = revision; 415 } 416 417 public String getParentRevision() { 418 return parentRevision; 419 } 420 421 public void setParentRevision(String parentRevision) { 422 this.parentRevision = parentRevision; 423 } 424 425 public void addMergedRevision(String mergedRevision) { 426 if (mergedRevisions == null) { 427 mergedRevisions = new LinkedHashSet<>(); 428 } 429 mergedRevisions.add(mergedRevision); 430 } 431 432 public Set<String> getMergedRevisions() { 433 return mergedRevisions == null ? Collections.emptySet() : mergedRevisions; 434 } 435 436 public void setMergedRevisions(Set<String> mergedRevisions) { 437 this.mergedRevisions = mergedRevisions; 438 } 439 440 /** 441 * {@inheritDoc} 442 */ 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 /** 535 * {@inheritDoc} 536 */ 537 public boolean equals(Object obj) { 538 if (obj instanceof ChangeSet) { 539 ChangeSet changeSet = (ChangeSet) obj; 540 541 if (toString().equals(changeSet.toString())) { 542 return true; 543 } 544 } 545 546 return false; 547 } 548 549 /** 550 * {@inheritDoc} 551 */ 552 public int hashCode() { 553 final int prime = 31; 554 int result = 1; 555 result = prime * result + ((author == null) ? 0 : author.hashCode()); 556 result = prime * result + ((comment == null) ? 0 : comment.hashCode()); 557 result = prime * result + ((date == null) ? 0 : date.hashCode()); 558 result = prime * result + ((parentRevision == null) ? 0 : parentRevision.hashCode()); 559 result = prime * result + ((mergedRevisions == null) ? 0 : mergedRevisions.hashCode()); 560 result = prime * result + ((files == null) ? 0 : files.hashCode()); 561 return result; 562 } 563 564 /** 565 * Remove a <code>]]></code> from comments (replace it with <code>] ] ></code>). 566 * 567 * @param message the message to modify 568 * @return a clean string 569 */ 570 private String removeCDataEnd(String message) { 571 // check for invalid sequence ]]> 572 int endCdata; 573 while (message != null && (endCdata = message.indexOf("]]>")) > -1) { 574 message = message.substring(0, endCdata) + "] ] >" + message.substring(endCdata + 3, message.length()); 575 } 576 return message; 577 } 578 579 /** 580 * <p>Escape the <code>toString</code> of the given object. 581 * For use in an attribute value.</p> 582 * <p> 583 * swiped from jakarta-commons/betwixt -- XMLUtils.java. 584 * 585 * @param value escape <code>value.toString()</code> 586 * @return text with characters restricted (for use in attributes) escaped 587 */ 588 public static String escapeValue(Object value) { 589 StringBuilder buffer = new StringBuilder(value.toString()); 590 for (int i = 0, size = buffer.length(); i < size; i++) { 591 switch (buffer.charAt(i)) { 592 case '<': 593 buffer.replace(i, i + 1, LESS_THAN_ENTITY); 594 size += 3; 595 i += 3; 596 break; 597 case '>': 598 buffer.replace(i, i + 1, GREATER_THAN_ENTITY); 599 size += 3; 600 i += 3; 601 break; 602 case '&': 603 buffer.replace(i, i + 1, AMPERSAND_ENTITY); 604 size += 4; 605 i += 4; 606 break; 607 case '\'': 608 buffer.replace(i, i + 1, APOSTROPHE_ENTITY); 609 size += 5; 610 i += 5; 611 break; 612 case '\"': 613 buffer.replace(i, i + 1, QUOTE_ENTITY); 614 size += 5; 615 i += 5; 616 break; 617 default: 618 } 619 } 620 return buffer.toString(); 621 } 622}