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}