You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

FieldDiffs.java 7.7KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293
  1. /*
  2. * SonarQube
  3. * Copyright (C) 2009-2021 SonarSource SA
  4. * mailto:info AT sonarsource DOT com
  5. *
  6. * This program is free software; you can redistribute it and/or
  7. * modify it under the terms of the GNU Lesser General Public
  8. * License as published by the Free Software Foundation; either
  9. * version 3 of the License, or (at your option) any later version.
  10. *
  11. * This program is distributed in the hope that it will be useful,
  12. * but WITHOUT ANY WARRANTY; without even the implied warranty of
  13. * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
  14. * Lesser General Public License for more details.
  15. *
  16. * You should have received a copy of the GNU Lesser General Public License
  17. * along with this program; if not, write to the Free Software Foundation,
  18. * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
  19. */
  20. package org.sonar.core.issue;
  21. import java.io.Serializable;
  22. import java.nio.charset.StandardCharsets;
  23. import java.util.Base64;
  24. import java.util.Date;
  25. import java.util.LinkedHashMap;
  26. import java.util.Map;
  27. import java.util.Objects;
  28. import javax.annotation.CheckForNull;
  29. import javax.annotation.Nullable;
  30. import org.apache.commons.lang.StringUtils;
  31. import static com.google.common.base.Strings.emptyToNull;
  32. import static com.google.common.base.Strings.isNullOrEmpty;
  33. /**
  34. * PLUGINS MUST NOT USE THIS CLASS, EXCEPT FOR UNIT TESTING.
  35. *
  36. * @since 3.6
  37. */
  38. public class FieldDiffs implements Serializable {
  39. private static final String CHAR_TO_ESCAPE = "|,{}=:";
  40. public static final String ASSIGNEE = "assignee";
  41. public static final String ENCODING_PREFIX = "{base64:";
  42. public static final String ENCODING_SUFFIX = "}";
  43. private final Map<String, Diff> diffs = new LinkedHashMap<>();
  44. private String issueKey;
  45. private String userUuid;
  46. private Date creationDate;
  47. public Map<String, Diff> diffs() {
  48. if (diffs.containsKey(ASSIGNEE)) {
  49. Map<String, Diff> result = new LinkedHashMap<>(diffs);
  50. result.put(ASSIGNEE, decode(result.get(ASSIGNEE)));
  51. return result;
  52. }
  53. return diffs;
  54. }
  55. public Diff get(String field) {
  56. if (field.equals(ASSIGNEE)) {
  57. return decode(diffs.get(ASSIGNEE));
  58. }
  59. return diffs.get(field);
  60. }
  61. @CheckForNull
  62. public String userUuid() {
  63. return userUuid;
  64. }
  65. public FieldDiffs setUserUuid(@Nullable String s) {
  66. this.userUuid = s;
  67. return this;
  68. }
  69. public Date creationDate() {
  70. return creationDate;
  71. }
  72. public FieldDiffs setCreationDate(Date creationDate) {
  73. this.creationDate = creationDate;
  74. return this;
  75. }
  76. @CheckForNull
  77. public String issueKey() {
  78. return issueKey;
  79. }
  80. public FieldDiffs setIssueKey(@Nullable String issueKey) {
  81. this.issueKey = issueKey;
  82. return this;
  83. }
  84. @SuppressWarnings("unchecked")
  85. public FieldDiffs setDiff(String field, @Nullable Serializable oldValue, @Nullable Serializable newValue) {
  86. Diff diff = diffs.get(field);
  87. if (field.equals(ASSIGNEE)) {
  88. oldValue = encodeField(oldValue);
  89. newValue = encodeField(newValue);
  90. }
  91. if (diff == null) {
  92. diff = new Diff(oldValue, newValue);
  93. diffs.put(field, diff);
  94. } else {
  95. diff.setNewValue(newValue);
  96. }
  97. return this;
  98. }
  99. public String toEncodedString() {
  100. return serialize(true);
  101. }
  102. @Override
  103. public String toString() {
  104. return serialize(false);
  105. }
  106. private String serialize(boolean shouldEncode) {
  107. StringBuilder sb = new StringBuilder();
  108. boolean notFirst = false;
  109. for (Map.Entry<String, Diff> entry : diffs.entrySet()) {
  110. if (notFirst) {
  111. sb.append(',');
  112. } else {
  113. notFirst = true;
  114. }
  115. sb.append(entry.getKey());
  116. sb.append('=');
  117. if (shouldEncode) {
  118. sb.append(entry.getValue().toEncodedString());
  119. } else {
  120. sb.append(entry.getValue().toString());
  121. }
  122. }
  123. return sb.toString();
  124. }
  125. public static FieldDiffs parse(@Nullable String s) {
  126. FieldDiffs diffs = new FieldDiffs();
  127. if (isNullOrEmpty(s)) {
  128. return diffs;
  129. }
  130. for (String field : s.split(",")) {
  131. if (field.isEmpty()) {
  132. continue;
  133. }
  134. String[] keyValues = field.split("=", 2);
  135. if (keyValues.length == 2) {
  136. String values = keyValues[1];
  137. int split = values.indexOf('|');
  138. if (split > -1) {
  139. diffs.setDiff(keyValues[0], emptyToNull(values.substring(0, split)), emptyToNull(values.substring(split + 1)));
  140. } else {
  141. diffs.setDiff(keyValues[0], null, emptyToNull(values));
  142. }
  143. } else {
  144. diffs.setDiff(keyValues[0], null, null);
  145. }
  146. }
  147. return diffs;
  148. }
  149. @SuppressWarnings("unchecked")
  150. Diff decode(Diff encoded) {
  151. return new Diff(
  152. decodeField(encoded.oldValue),
  153. decodeField(encoded.newValue)
  154. );
  155. }
  156. private static Serializable decodeField(@Nullable Serializable field) {
  157. if (field == null || !isEncoded(field)) {
  158. return field;
  159. }
  160. String encodedField = field.toString();
  161. String value = encodedField.substring(ENCODING_PREFIX.length(), encodedField.indexOf(ENCODING_SUFFIX));
  162. return new String(Base64.getDecoder().decode(value), StandardCharsets.UTF_8);
  163. }
  164. private static Serializable encodeField(@Nullable Serializable field) {
  165. if (field == null || !shouldEncode(field.toString())) {
  166. return field;
  167. }
  168. return ENCODING_PREFIX + Base64.getEncoder().encodeToString(field.toString().getBytes(StandardCharsets.UTF_8)) + ENCODING_SUFFIX;
  169. }
  170. private static boolean shouldEncode(String field) {
  171. return !field.isEmpty() && !isEncoded(field) && containsCharToEscape(field);
  172. }
  173. private static boolean containsCharToEscape(Serializable s) {
  174. return StringUtils.containsAny(s.toString(), CHAR_TO_ESCAPE);
  175. }
  176. private static boolean isEncoded(Serializable field) {
  177. return field.toString().startsWith(ENCODING_PREFIX) && field.toString().endsWith(ENCODING_SUFFIX);
  178. }
  179. public static class Diff<T extends Serializable> implements Serializable {
  180. private T oldValue;
  181. private T newValue;
  182. public Diff(@Nullable T oldValue, @Nullable T newValue) {
  183. this.oldValue = oldValue;
  184. this.newValue = newValue;
  185. }
  186. @CheckForNull
  187. public T oldValue() {
  188. return oldValue;
  189. }
  190. @CheckForNull
  191. public Long oldValueLong() {
  192. return toLong(oldValue);
  193. }
  194. @CheckForNull
  195. public T newValue() {
  196. return newValue;
  197. }
  198. @CheckForNull
  199. public Long newValueLong() {
  200. return toLong(newValue);
  201. }
  202. void setNewValue(@Nullable T t) {
  203. this.newValue = t;
  204. }
  205. @CheckForNull
  206. private static Long toLong(@Nullable Serializable value) {
  207. if (value != null && !"".equals(value)) {
  208. try {
  209. return Long.valueOf((String) value);
  210. } catch (ClassCastException e) {
  211. return (Long) value;
  212. }
  213. }
  214. return null;
  215. }
  216. private String toEncodedString() {
  217. return serialize(true);
  218. }
  219. @Override
  220. public String toString() {
  221. return serialize(false);
  222. }
  223. private String serialize(boolean shouldEncode) {
  224. StringBuilder sb = new StringBuilder();
  225. if (oldValue != null) {
  226. sb.append(shouldEncode ? oldValue : decodeField(oldValue));
  227. sb.append('|');
  228. }
  229. if (newValue != null) {
  230. sb.append(shouldEncode ? newValue : decodeField(newValue));
  231. }
  232. return sb.toString();
  233. }
  234. @Override
  235. public boolean equals(Object o) {
  236. if (this == o) {
  237. return true;
  238. }
  239. if (o == null || getClass() != o.getClass()) {
  240. return false;
  241. }
  242. Diff<?> diff = (Diff<?>) o;
  243. return Objects.equals(oldValue, diff.oldValue) &&
  244. Objects.equals(newValue, diff.newValue);
  245. }
  246. @Override
  247. public int hashCode() {
  248. return Objects.hash(oldValue, newValue);
  249. }
  250. }
  251. }