001/*-------------------------------------------------------------------------+ 002| | 003| Copyright 2005-2011 the ConQAT Project | 004| | 005| Licensed under the Apache License, Version 2.0 (the "License"); | 006| you may not use this file except in compliance with the License. | 007| You may obtain a copy of the License at | 008| | 009| http://www.apache.org/licenses/LICENSE-2.0 | 010| | 011| Unless required by applicable law or agreed to in writing, software | 012| distributed under the License is distributed on an "AS IS" BASIS, | 013| WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | 014| See the License for the specific language governing permissions and | 015| limitations under the License. | 016+-------------------------------------------------------------------------*/ 017package org.conqat.engine.index.shared; 018 019import java.io.Serializable; 020import java.util.Arrays; 021import java.util.Comparator; 022import java.util.Objects; 023 024import org.conqat.engine.commons.util.JsonUtils; 025import org.conqat.engine.core.core.ConQATException; 026import org.conqat.engine.service.shared.ServiceUtils; 027import org.conqat.lib.commons.assertion.CCSMAssert; 028import org.conqat.lib.commons.io.ByteArrayUtils; 029import org.conqat.lib.commons.js_export.ExportToJavaScript; 030import org.conqat.lib.commons.string.StringUtils; 031 032import com.fasterxml.jackson.annotation.JsonCreator; 033import com.fasterxml.jackson.annotation.JsonProperty; 034 035/** 036 * Immutable class describing a single commit by its branch name and a 037 * timestamp. The timestamp must be unique within the branch. They are 038 * comparable by timestamp, where equal timestamps are resolved alphabetically 039 * by the branch name. 040 * 041 * <strong>This class is used for communication with IDE clients (via the 042 * {@link org.conqat.engine.service.shared.client.IdeServiceClient}), so special 043 * care has to be taken when changing its signature!</strong> 044 */ 045@ExportToJavaScript 046public class CommitDescriptor implements Serializable, Comparable<CommitDescriptor> { 047 048 /** 049 * The separator used between branch name and timestamp in 050 * {@link #toBranchTimestampKeyWithSeparator()}. 051 */ 052 public static final byte[] SEPARATOR = { 1, 2, 1 }; 053 054 /** 055 * Name of the branch if no branch is specified. 056 * 057 * @see org.conqat.engine.persistence.store.hist.HistoryAccessOption#NO_BRANCH_NAME 058 */ 059 // HistoryAccessOption is not accessible in all project where this class is 060 // used as external source file 061 @SuppressWarnings("javadoc") 062 private static final String NO_BRANCH_NAME = "##no-branch##"; 063 064 /** Version for serialization. */ 065 private static final long serialVersionUID = 1L; 066 067 /** Special value to indicate the HEAD of a branch. */ 068 public static final String HEAD_TIMESTAMP = "HEAD"; 069 070 /** 071 * A comparator for comparison by timestamps. Null commits are ordered to the 072 * front. 073 * 074 * If there are commits with identical timestamps, we order them by comparing 075 * the branch names to guarantee a stable order. This should only happen in 076 * special cases since teamscale ensures that no timestamp is given to two 077 * commits. 078 */ 079 public static final Comparator<CommitDescriptor> BY_TIMESTAMP_COMPARATOR = Comparator.nullsFirst( 080 Comparator.comparingLong(CommitDescriptor::getTimestamp).thenComparing(CommitDescriptor::getBranchName)); 081 082 /** The name of the JSON property name for {@link #branchName}. */ 083 private static final String BRANCH_NAME_PROPERTY = "branchName"; 084 085 /** The name of the JSON property name for {@link #timestamp}. */ 086 private static final String TIMESTAMP_PROPERTY = "timestamp"; 087 088 /** The name of the branch. */ 089 @JsonProperty(BRANCH_NAME_PROPERTY) 090 private final String branchName; 091 092 /** The timestamp on the branch. */ 093 @JsonProperty(TIMESTAMP_PROPERTY) 094 private final long timestamp; 095 096 /** 097 * Constructor. <br/> 098 * use {@link CommitDescriptor#createUnbranchedDescriptor(long)} to create a 099 * unbranched commit descriptor. 100 */ 101 @JsonCreator 102 public CommitDescriptor(@JsonProperty(BRANCH_NAME_PROPERTY) String branchName, 103 @JsonProperty(TIMESTAMP_PROPERTY) long timestamp) { 104 CCSMAssert.isTrue(timestamp >= 0, "Timestamp must be >= 0 but is " + timestamp); 105 CCSMAssert.isNotNull(branchName); 106 107 this.branchName = branchName; 108 this.timestamp = timestamp; 109 } 110 111 public CommitDescriptor(CommitDescriptor other) { 112 this(other.branchName, other.timestamp); 113 } 114 115 /** 116 * Create a {@link CommitDescriptor} without branch specification. <br/> 117 * Should be used with caution. 118 */ 119 public static CommitDescriptor createUnbranchedDescriptor(long timestamp) { 120 return new CommitDescriptor(NO_BRANCH_NAME, timestamp); 121 } 122 123 /** 124 * Create a copy of this commit descriptor with <code>timestamp - 1</code>. 125 * 126 * Using this CommitDescriptor to store a new commit might overwrite an existing 127 * commit if the timestamp is already used. 128 */ 129 public CommitDescriptor cloneWithDecrementedTimestamp() { 130 return new CommitDescriptor(this.branchName, this.timestamp - 1); 131 } 132 133 /** 134 * Create a copy of this commit descriptor with <code>timestamp + 1</code>. 135 * 136 * Using this CommitDescriptor to store a new commit might overwrite an existing 137 * commit if the timestamp is already used. 138 */ 139 public CommitDescriptor cloneWithIncrementedTimestamp() { 140 return new CommitDescriptor(this.branchName, this.timestamp + 1); 141 } 142 143 /** 144 * @see #branchName 145 */ 146 public String getBranchName() { 147 return branchName; 148 } 149 150 /** Return true if no branch is specified. */ 151 public boolean isUnbranched() { 152 return NO_BRANCH_NAME.equals(this.branchName); 153 } 154 155 /** Returns whether this commit is a head timestamp */ 156 public boolean isHeadCommit() { 157 return timestamp == Long.MAX_VALUE; 158 } 159 160 /** 161 * @see #timestamp 162 */ 163 public long getTimestamp() { 164 return timestamp; 165 } 166 167 /** {@inheritDoc} */ 168 @Override 169 public boolean equals(Object obj) { 170 if (obj instanceof CommitDescriptor) { 171 CommitDescriptor other = (CommitDescriptor) obj; 172 return other.timestamp == timestamp && Objects.equals(this.branchName, other.branchName); 173 } 174 return false; 175 } 176 177 /** {@inheritDoc} */ 178 @Override 179 public int hashCode() { 180 return Objects.hashCode(branchName) ^ Long.hashCode(timestamp); 181 } 182 183 /** {@inheritDoc} */ 184 @Override 185 public int compareTo(CommitDescriptor other) { 186 if (timestamp == other.timestamp) { 187 return branchName.compareTo(other.branchName); 188 } 189 return Long.compare(timestamp, other.timestamp); 190 } 191 192 /** Converts this to a JSON representation. */ 193 public String toJson() { 194 return JsonUtils.serializeToJSON(this); 195 } 196 197 /** Reads a {@link CommitDescriptor} from a JSON string. */ 198 public static CommitDescriptor fromJson(String json) throws ConQATException { 199 return JsonUtils.deserializeFromJsonWithNullCheck(json, CommitDescriptor.class); 200 } 201 202 /** {@inheritDoc} */ 203 @Override 204 public String toString() { 205 return branchName + "@" + timestamp; 206 } 207 208 /** Returns a timestamp+branchName key/byte[] representation. */ 209 public byte[] toTimestampBranchKey() { 210 return ByteArrayUtils.concat(ByteArrayUtils.longToByteArray(getTimestamp()), 211 StringUtils.stringToBytes(getBranchName())); 212 } 213 214 /** Parses a commit descriptor from its toString() representation. */ 215 public static CommitDescriptor fromStringRepresentation(String representation) { 216 int separatorPosition = representation.lastIndexOf("@"); 217 CCSMAssert.isTrue(separatorPosition >= 0, 218 () -> "Invalid string representation of commit descriptor: " + representation); 219 String branch = representation.substring(0, separatorPosition); 220 String timestamp = representation.substring(separatorPosition + 1); 221 return new CommitDescriptor(branch, Long.parseLong(timestamp)); 222 } 223 224 /** Parses a timestamp+branchName key/byte[] representation. */ 225 public static CommitDescriptor fromTimestampBranchKey(byte[] key) { 226 long timestamp = ByteArrayUtils.byteArrayToLong(Arrays.copyOf(key, Long.BYTES)); 227 String branchName = StringUtils.bytesToString(Arrays.copyOfRange(key, Long.BYTES, key.length)); 228 return new CommitDescriptor(branchName, timestamp); 229 } 230 231 /** 232 * Returns a branchName+timestamp key/byte[] representation. NOTE: This is 233 * *DANGEROUS* since keys generated with this function may cause unwanted branch 234 * names (prefixes of wanted branch names) to be returned from store scans 235 * (TS-16367). To protect against this, use 236 * {@link #toBranchTimestampKeyWithSeparator()}. 237 */ 238 public byte[] toBranchTimestampKey() { 239 return ByteArrayUtils.concat(StringUtils.stringToBytes(getBranchName()), 240 ByteArrayUtils.longToByteArray(getTimestamp())); 241 } 242 243 /** 244 * Returns a branchName+timestamp key/byte[] representation with 245 * {@link #SEPARATOR} in between. 246 */ 247 public byte[] toBranchTimestampKeyWithSeparator() { 248 return ByteArrayUtils.concat(StringUtils.stringToBytes(getBranchName()), SEPARATOR, 249 ByteArrayUtils.longToByteArray(getTimestamp())); 250 } 251 252 /** Parses a branchN+timestamp key/byte[] representation. */ 253 public static CommitDescriptor fromBranchTimestampKey(byte[] key) { 254 String branchName = StringUtils.bytesToString(Arrays.copyOf(key, key.length - Long.BYTES)); 255 long timestamp = ByteArrayUtils.byteArrayToLong(Arrays.copyOfRange(key, key.length - Long.BYTES, key.length)); 256 return new CommitDescriptor(branchName, timestamp); 257 } 258 259 /** Creates a commit descriptor for the latest revision on a branch */ 260 public static CommitDescriptor latestOnBranch(String branchName) { 261 return new CommitDescriptor(branchName, Long.MAX_VALUE); 262 } 263 264 /** 265 * Returns a format used for service calls ("branch:timestamp", or "timestamp" 266 * if no branch is specified). 267 * 268 * @see #toEncodedPathParam() 269 * @see #toEncodedQueryParam() 270 */ 271 public String toServiceCallFormat() { 272 String result = branchName + ":"; 273 if (NO_BRANCH_NAME.contentEquals(branchName)) { 274 result = StringUtils.EMPTY_STRING; 275 } 276 277 if (timestamp == Long.MAX_VALUE) { 278 return result + HEAD_TIMESTAMP; 279 } 280 return result + timestamp; 281 } 282 283 /** 284 * Returns a url-encoded format used for service calls ("branch:timestamp", or 285 * "timestamp" if no branch is specified). The format is suitable for calls that 286 * include the commit as query parameter or in the arguments of the navigation 287 * hash. 288 */ 289 public String toEncodedQueryParam() { 290 return ServiceUtils.encodeQueryParameter(toServiceCallFormat()); 291 } 292 293 /** 294 * Returns a url-encoded format used for service calls ("branch:timestamp", or 295 * "timestamp" if no branch is specified). The format is suitable for calls that 296 * include the commit as path parameter. 297 */ 298 public String toEncodedPathParam() { 299 return ServiceUtils.encodePathSegment(toServiceCallFormat()); 300 } 301}