public static IssueChangeDto of(String issueKey, FieldDiffs diffs) {
IssueChangeDto dto = newDto(issueKey);
dto.setChangeType(IssueChangeDto.TYPE_FIELD_CHANGE);
- dto.setChangeData(diffs.toString());
+ dto.setChangeData(diffs.toEncodedString());
dto.setUserUuid(diffs.userUuid());
Date createdAt = requireNonNull(diffs.creationDate(), "Diffs created at must not be null");
dto.setIssueChangeCreationDate(createdAt.getTime());
FieldDiffs.Diff value = diff.getValue();
Changelog.Diff.Builder diffBuilder = Changelog.Diff.newBuilder();
String key = diff.getKey();
+ String oldValue = value.oldValue() != null ? value.oldValue().toString() : null;
+ String newValue = value.newValue() != null ? value.newValue().toString() : null;
if (key.equals(FILE)) {
diffBuilder.setKey(key);
- setNullable(results.getFileLongName(emptyToNull(value.newValue().toString())), diffBuilder::setNewValue);
- setNullable(results.getFileLongName(emptyToNull(value.oldValue().toString())), diffBuilder::setOldValue);
+ setNullable(results.getFileLongName(emptyToNull(newValue)), diffBuilder::setNewValue);
+ setNullable(results.getFileLongName(emptyToNull(oldValue)), diffBuilder::setOldValue);
} else {
diffBuilder.setKey(key.equals(TECHNICAL_DEBT) ? EFFORT_CHANGELOG_KEY : key);
- setNullable(emptyToNull(value.newValue().toString()), diffBuilder::setNewValue);
- setNullable(emptyToNull(value.oldValue().toString()), diffBuilder::setOldValue);
+ setNullable(emptyToNull(newValue), diffBuilder::setNewValue);
+ setNullable(emptyToNull(oldValue), diffBuilder::setOldValue);
}
return diffBuilder.build();
};
import com.google.common.base.Splitter;
import com.google.common.collect.Maps;
import java.io.Serializable;
+import java.nio.charset.StandardCharsets;
+import java.util.Base64;
import java.util.Date;
import java.util.Map;
import java.util.Objects;
import javax.annotation.CheckForNull;
import javax.annotation.Nullable;
+import org.apache.commons.lang.StringUtils;
import static com.google.common.base.Strings.emptyToNull;
import static com.google.common.base.Strings.isNullOrEmpty;
public class FieldDiffs implements Serializable {
public static final Splitter FIELDS_SPLITTER = Splitter.on(',').omitEmptyStrings();
+ private static final String CHAR_TO_ESCAPE = "|,{}=:";
private String issueKey;
private String userUuid;
private Date creationDate;
+ public static final String ASSIGNEE = "assignee";
+ public static final String ENCODING_PREFIX = "{base64:";
+ public static final String ENCODING_SUFFIX = "}";
private final Map<String, Diff> diffs = Maps.newLinkedHashMap();
public Map<String, Diff> diffs() {
+ if (diffs.containsKey(ASSIGNEE)) {
+ Map<String, Diff> result = Maps.newLinkedHashMap(diffs);
+ result.put(ASSIGNEE, decode(result.get(ASSIGNEE)));
+ return result;
+ }
return diffs;
}
public Diff get(String field) {
+ if (field.equals(ASSIGNEE)) {
+ return decode(diffs.get(ASSIGNEE));
+ }
return diffs.get(field);
}
@SuppressWarnings("unchecked")
public FieldDiffs setDiff(String field, @Nullable Serializable oldValue, @Nullable Serializable newValue) {
Diff diff = diffs.get(field);
+ if (field.equals(ASSIGNEE)) {
+ oldValue = encodeField(oldValue);
+ newValue = encodeField(newValue);
+ }
if (diff == null) {
diff = new Diff(oldValue, newValue);
diffs.put(field, diff);
return this;
}
+ public String toEncodedString() {
+ return serialize(true);
+ }
+
@Override
public String toString() {
+ return serialize(false);
+ }
+
+ private String serialize(boolean shouldEncode) {
StringBuilder sb = new StringBuilder();
boolean notFirst = false;
for (Map.Entry<String, Diff> entry : diffs.entrySet()) {
}
sb.append(entry.getKey());
sb.append('=');
- sb.append(entry.getValue().toString());
+ if (shouldEncode) {
+ sb.append(entry.getValue().toEncodedString());
+ } else {
+ sb.append(entry.getValue().toString());
+ }
}
return sb.toString();
}
}
Iterable<String> fields = FIELDS_SPLITTER.split(s);
for (String field : fields) {
- String[] keyValues = field.split("=");
+ String[] keyValues = field.split("=", 2);
if (keyValues.length == 2) {
String values = keyValues[1];
int split = values.indexOf('|');
if (split > -1) {
- diffs.setDiff(keyValues[0], values.substring(0, split), values.substring(split +1));
+ diffs.setDiff(keyValues[0], emptyToNull(values.substring(0, split)), emptyToNull(values.substring(split + 1)));
} else {
- diffs.setDiff(keyValues[0], "", emptyToNull(values));
+ diffs.setDiff(keyValues[0], null, emptyToNull(values));
}
} else {
- diffs.setDiff(keyValues[0], "", "");
+ diffs.setDiff(keyValues[0], null, null);
}
}
return diffs;
}
+ @SuppressWarnings("unchecked")
+ Diff decode(Diff encoded) {
+ return new Diff(
+ decodeField(encoded.oldValue),
+ decodeField(encoded.newValue)
+ );
+ }
+
+ private static Serializable decodeField(@Nullable Serializable field) {
+ if (field == null || !isEncoded(field)) {
+ return field;
+ }
+
+ String encodedField = field.toString();
+ String value = encodedField.substring(ENCODING_PREFIX.length(), encodedField.indexOf(ENCODING_SUFFIX));
+ return new String(Base64.getDecoder().decode(value), StandardCharsets.UTF_8);
+ }
+
+ private static Serializable encodeField(@Nullable Serializable field) {
+ if (field == null || !shouldEncode(field.toString())) {
+ return field;
+ }
+
+ return ENCODING_PREFIX + Base64.getEncoder().encodeToString(field.toString().getBytes(StandardCharsets.UTF_8)) + ENCODING_SUFFIX;
+ }
+
+ private static boolean shouldEncode(String field) {
+ return !field.isEmpty() && !isEncoded(field) && containsCharToEscape(field);
+ }
+
+ private static boolean containsCharToEscape(Serializable s) {
+ return StringUtils.containsAny(s.toString(), CHAR_TO_ESCAPE);
+ }
+
+ private static boolean isEncoded(Serializable field) {
+ return field.toString().startsWith(ENCODING_PREFIX) && field.toString().endsWith(ENCODING_SUFFIX);
+ }
+
public static class Diff<T extends Serializable> implements Serializable {
private T oldValue;
private T newValue;
return toLong(newValue);
}
- void setNewValue(T t) {
+ void setNewValue(@Nullable T t) {
this.newValue = t;
}
return null;
}
+ private String toEncodedString() {
+ return serialize(true);
+ }
+
@Override
public String toString() {
- // TODO escape , and | characters
+ return serialize(false);
+ }
+
+ private String serialize(boolean shouldEncode) {
StringBuilder sb = new StringBuilder();
if (oldValue != null) {
- sb.append(oldValue.toString());
+ sb.append(shouldEncode ? oldValue : decodeField(oldValue));
sb.append('|');
}
if (newValue != null) {
- sb.append(newValue.toString());
+ sb.append(shouldEncode ? newValue : decodeField(newValue));
}
return sb.toString();
}
public class FieldDiffsTest {
- FieldDiffs diffs = new FieldDiffs();
+ private static final String NAME_WITH_RESERVED_CHARS = "name,with|reserved=chars:";
+ private static final String ENCODED_NAME_WITH_RESERVED_CHARS = "{base64:bmFtZSx3aXRofHJlc2VydmVkPWNoYXJzOg==}";
+
+ private FieldDiffs diffs = new FieldDiffs();
@Test
public void diffs_should_be_empty_by_default() {
@Test
public void diff_with_long_values() {
- diffs.setDiff("technicalDebt", 50l, "100");
+ diffs.setDiff("technicalDebt", 50L, "100");
FieldDiffs.Diff diff = diffs.diffs().get("technicalDebt");
- assertThat(diff.oldValueLong()).isEqualTo(50l);
- assertThat(diff.newValueLong()).isEqualTo(100l);
+ assertThat(diff.oldValueLong()).isEqualTo(50L);
+ assertThat(diff.newValueLong()).isEqualTo(100L);
}
@Test
assertThat(diff.newValueLong()).isNull();
}
+ @Test
+ public void diff_with_assignee() {
+ diffs.setDiff("assignee", "oldAssignee", NAME_WITH_RESERVED_CHARS);
+
+ FieldDiffs.Diff diff = diffs.diffs().get("assignee");
+ assertThat(diff.oldValue()).isEqualTo("oldAssignee");
+ assertThat(diff.newValue()).isEqualTo(NAME_WITH_RESERVED_CHARS);
+ }
+
+ @Test
+ public void get() {
+ diffs.setDiff("severity", "BLOCKER", "INFO");
+
+ FieldDiffs.Diff diff = diffs.get("severity");
+ assertThat(diff.oldValue()).isEqualTo("BLOCKER");
+ assertThat(diff.newValue()).isEqualTo("INFO");
+ }
+
+ @Test
+ public void get_with_assignee() {
+ diffs.setDiff("assignee", "oldAssignee", NAME_WITH_RESERVED_CHARS);
+
+ FieldDiffs.Diff diff = diffs.get("assignee");
+ assertThat(diff.oldValue()).isEqualTo("oldAssignee");
+ assertThat(diff.newValue()).isEqualTo(NAME_WITH_RESERVED_CHARS);
+ }
+
@Test
public void test_diff_by_key() {
diffs.setDiff("severity", "BLOCKER", "INFO");
assertThat(diffs.toString()).isEqualTo("severity=BLOCKER|INFO,resolution=OPEN|FIXED");
}
+ @Test
+ public void test_toEncodedString() {
+ diffs.setDiff("assignee", "oldAssignee", NAME_WITH_RESERVED_CHARS);
+ diffs.setDiff("resolution", "OPEN", "FIXED");
+
+ assertThat(diffs.toEncodedString()).isEqualTo("assignee=oldAssignee|" + ENCODED_NAME_WITH_RESERVED_CHARS + ",resolution=OPEN|FIXED");
+ }
+
@Test
public void test_toString_with_null_values() {
diffs.setDiff("severity", null, "INFO");
assertThat(diff.newValue()).isEqualTo("FIXED");
diff = diffs.diffs().get("donut");
- assertThat(diff.oldValue()).isEqualTo("");
+ assertThat(diff.oldValue()).isEqualTo(null);
assertThat(diff.newValue()).isEqualTo("new");
diff = diffs.diffs().get("gambas");
- assertThat(diff.oldValue()).isEqualTo("");
+ assertThat(diff.oldValue()).isEqualTo(null);
assertThat(diff.newValue()).isEqualTo("miam");
diff = diffs.diffs().get("acme");
assertThat(diff.oldValue()).isEqualTo("old");
- assertThat(diff.newValue()).isEqualTo("");
+ assertThat(diff.newValue()).isEqualTo(null);
+ }
+
+ @Test
+ public void test_parse_encoded_assignee() {
+ diffs = FieldDiffs.parse("severity=BLOCKER|INFO,assignee=oldValue|" + ENCODED_NAME_WITH_RESERVED_CHARS);
+ assertThat(diffs.diffs()).hasSize(2);
+
+ FieldDiffs.Diff diff = diffs.diffs().get("severity");
+ assertThat(diff.oldValue()).isEqualTo("BLOCKER");
+ assertThat(diff.newValue()).isEqualTo("INFO");
+
+ diff = diffs.diffs().get("assignee");
+ assertThat(diff.oldValue()).isEqualTo("oldValue");
+ assertThat(diff.newValue()).isEqualTo(NAME_WITH_RESERVED_CHARS);
}
@Test
assertThat(diffs.diffs()).hasSize(2);
FieldDiffs.Diff diff = diffs.diffs().get("severity");
- assertThat(diff.oldValue()).isEqualTo("");
+ assertThat(diff.oldValue()).isEqualTo(null);
assertThat(diff.newValue()).isEqualTo("INFO");
diff = diffs.diffs().get("resolution");
- assertThat(diff.oldValue()).isEqualTo("");
- assertThat(diff.newValue()).isEqualTo("");
+ assertThat(diff.oldValue()).isEqualTo(null);
+ assertThat(diff.newValue()).isEqualTo(null);
}
@Test