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